Alec Aivazis
PathCreated with Sketch.GroupCreated with Sketch.

A Guide to GraphQL Schema Federation, Part 2

Authorization

Welcome to the second part in a series of guides on building a distributed GraphQL API. In this post, we will add a very simple mechanism for users to log in and out of our system. We will then look at how the nautilus gateway allows us to add fields to the root of our Query type that we'll use to query for the current user.

Please keep in mind that the code samples in this post are not secure enough for production as is. I tried my hardest to be as representative of the "real world" as I could without compromising the clarity of the examples.

This tutorial requires working installations of Go and Node on your computer. If you run into trouble setting either up, feel free to reach out!

Our Services

Like before, we're going to start with the two services from the starter project:

git clone https://github.com/alecaivazis/schema-federation-demo && git checkout  auth

Our core application is made up of two services, one that manages our cat photos, and another that is responsible for our auctions. These two services are merged in a third known as the "gateway" which acts as the single point of entry for our application. In this post, we have a fourth service that is responsible for authenticating users.

Since our example is small, the auth service only needs to add a single mutation to our API: login. As expected, login takes username and password and if the combination is valid, returns a token which the client can use to identify itself later. Once the client has this token, it will include it in a header on all requests to the API. The gateway will forward this value to the other services which are responsible for imposing any authorization constraints according to their domain. For example, it's up to the photo service to define if user 1 can resolve User.favoritePhoto.

Building a Custom Gateway

By default, the nautilus gateway does not forward any headers to the backend services when resolving queries. To accomplish this, we will have to build a custom binary which requires writing some Go. If you've never written Go before, don't worry! It should be straightforward to read if you know JavaScript.

If you look in gateway.go you'll see that a lot of the necessary boilerplate for creating a gateway instance already in place. We first have to introspect our remote backends, instantiate the gateway with the results, and wire it up to our http router.

Before we get too far, let's verify everything is working. Start the other services following the instructions in the README and then run the gateway with go run gateway.go. You should now see a message with a URL for you to visit and interact with our system. Log in as user 1 with the following query and hold onto the auth token returned:

mutation {
    login(input: { username: "1", password: "1" }) {
        authToken
    }
}

Forwarding User Information to our Backends

Now that the client has a token to claim an identity, we have to process it in our gateway and send the services the information they need to perform any kind of authorization checks. To do this, we're going to define a nautilus.RequestMiddleware which adds a header to the outbound request based on some value in the request's context. For that to work, we first have to grab the value of the header from the incoming request and save it in the execution context. Let's begin by defining a middleware that will wraps our playground route and will pull the value of the incoming Authorization header, save it under the "user-id" key of the request context, and invoke the next handler:

func withUserInfo(handler http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // look up the value of the Authorization header
        tokenValue := r.Header.Get("Authorization")

        // here is where you would perform some kind of validation on the token
        // but we're going to skip that for this example and save it as the
        // id directly. PLEASE, DO NOT DO THIS IN PRODUCTION.

        // invoke the handler with the new context
        handler.ServeHTTP(w, r.WithContext(
            context.WithValue(r.Context(), "user-id", tokenValue),
        ))
    })
}

Don't forget to wrap the route we defined earlier with this middleware:

// add the playground endpoint to the router
http.HandleFunc("/graphql", withUserInfo(gw.PlaygroundHandler))

Now that we have the value of the Authorization header in our request context, we can define a gateway.RequestMiddleware that pulls out the value of the "user-id" key we set earlier and sets a header on the internal network request made between the gateway and the other services:

var forwardUserID = gateway.RequestMiddleware(func(r *http.Request) error {
    // the initial context of the request is set to match the one we modified earlier

    // we are safe to extract the value we saved in context
    if userID := r.Context().Value("user-id"); userID != nil {
        // set the header with the value we pulled out of context
        r.Header.Set("USER_ID", userID.(string))
    }

    // there was no error
    return nil
})

Include the middleware to our call to gateway.New:

// create the gateway instance
gw, err := gateway.New(schemas, gateway.WithMiddlewares(forwardUserID))

Now that our gateway is forwarding the user's ID along with their requests, the only thing left to do is go to our backend service and do something with that value. For example, let's make it so that you cannot see the photoGallery of another user. To do this, open up photoService.js and modify the User.photoGallery resolver to look like:

User: {
    ...otherResolvers,
    photoGallery: (user, _, { headers }) => {
        // if the USER_ID header is not the same ID as the user
        if (user.id !== headers.USER_ID) {
            throw new Error("Sorry, you cannot view someone else's photo gallery.")
        }

        // the current user can see this user's photo gallery
        return root.photoGallery.map(id => photos[id])
    }
}

With this in place, you should be able to verify the intended behaviour. Take the token you wrote down earlier and set it as the value of the Authorization header in the playground. Now, you should see an error when you try to fire this query since you are logged in as user "1" and looking for the photoGallery of user "2":

{
    node(id: "VXNlcjoy") {
        ... on User {
            photoGallery {
                url
            }
        }
    }
}

Querying for the Current User

While the bulk of the logic and computation in a distributed API happens in the services "behind" the gateway, there are certain situations where it makes sense for fields to be resolved by the gateeway. One such instance that is very common in APIs with authorization is the ability to query for the current user. By convention, this field is called Query.viewer and it allows the current user to ask for things of themselves:

{
    viewer {
        photoGallery {
            url
        }
    }
}

Adding these kinds of fields to the nautilus gateway does not take more than a few lines:

import "github.com/vektah/gqlparser/ast"

var viewerField = &gateway.QueryField{
    Name: "viewer",
    Type: ast.NamedType("User", &ast.Position{}),
    // this function must return the ID of the object that the field resolves to
    Resolver: func(ctx context.Context, args map[string]interface{}) (string, error) {
        // for now just return the value in context
        return ctx.Value("user-id").(string), nil
    },
}

The only thing left is to tell the gateway to use this field along with our middleware from before:

gw, err := gateway.New(schemas,
    gateway.WithMiddlewares(forwardUserID),
    gateway.WithQueryFields(viewerField)
)

We should now be able to query for our favorite photo with the following query:

{
    viewer {
        favoritePhoto {
            url
        }
    }
}

Conclusion

In this blog post, we built a custom gateway that took an auth token provided by mutation and forwarded the underlying user's ID to our backends. We then used this information in our service to define domain-specific authorization logic. And finally, we saw that we could also use this token to define a field on the gateway that resolves to the current user without forcing an extra network hop that resolves the field at the service level.

While the example was trimmed down to the simple bare necessities, hopefully, it illustrated how the different moving parts work together. Please remember that the specifics such as verifying passwords, creating and verifying the token are not meant to demonstrate best practices and were placeholders for industry standards. If you are not sure what the better solution is, you can find me on the gopher's slack any time.

That's it for now! Thank you so much for getting this far with me. If you have room for more, head over to the next post where we explore an optimization that speeds up our query resolution times.

As always, if this is where we part ways, I hope you have an excellent rest of your day.