When building an API, it's often a bad idea to serve all data to everyone on the Internet. For business reasons we need to check, who is a paying customer and for security and privacy reasons we need to restrict access to parts of our system.
We've already written an article about authentication and authorization with REST APIs.
In this article, we will dive into the auth topic with GraphQL.
The Difference Between Authentication and Authorization
Both are often lumped together with the word Auth as in "We need to add auth capabilities to our system."
While the two topics are intertwined, they cover different aspects.
Authentication is about knowing who wants to do something with our system.
We can solve this problem with a login via username/email and password or social logins with GitHub or Facebook etc.
Authorization is about knowing what they are allowed to do with our system.
This problem is more involved in that it deeply dependent on our business cases. While it isn't apparent, authorization is business logic and should be treated as such.
Authentication, Authorization, and GraphQL
Where do we place this logic in our GraphQL APIs?
The opinion of the GraphQL creators is to "delegate authorization logic to the business logic layer". They don't have any strong opinion on authentication besides the fact that creating "fully hydrated user-objects" instead of passing auth-tokens down to our business logic.
What does this mean? Where is our business logic located? Where would we create these user-objects?
Structure of a GraphQL System
A GraphQL system could be structured as seen in the following picture:
On the left, we have different types of middleware that apply some logic to all of our requests.
In the middle we have the GraphQL engine, that figures out how to translate our GraphQL queries into calls to the right resolver functions with the help of our GraphQL Schema. In most frameworks, like the Express framework for Node.js, the GraphQL Engine is also implemented as middleware.
On the right, we have different types of resolver functions that connect the GraphQL engine to our business logic. A resolver gets executed by the GraphQL engine when its corresponding data type was queried.
The business logic could be one monolithic system or a bunch of microservices.
Integration of Authentication into GraphQL
To authorize a request, we need access to the current users' identity in our business logic. This requires us to put the authentication logic into a place that gets executed for all our requests. If we look at the graphic above, this makes the middleware-part of our system the perfect place for it.
Another nice thing of this approach: It's completely independent of GraphQL, so we can re-use our REST skills and re-apply them!
If you need to freshen-up your REST authentication skills, read our previous article.
Integration of Authorization into GraphQL
To get a sense of this, let's look at a simple implementation of middleware, a REST resource handler function, and a GraphQL resolver function. For this example I will use Node.js with the Express framework and the Apollo Server.
First, our business logic:
const getMessage = (id, user) => {
const message = dataStore[id];
if (message.recipient.id === user.id) return message;
if (message.sender.id === user.id) return message;
throw new Error("no permission");
};
It loads a message by ID from in-memory storage. If the user is the recipient or the sender of this message, they're allowed to read it. Otherwise, we throw an error.
Second, the authentication middleware:
const authMiddleware = (request, response, next) => {
const token = request.get("authorization");
const user = loadUser(token);
if (user) {
request.user = user;
next();
}
response.status(401).end();
};
It loads a token from a request header and uses it to load the current user. If that worked, it adds this user-object to the request so that it can be used later.
Now, let's look at the REST implementation.
expressApp.use(authMiddleware);
expressApp.get("/message/:id", (request, response) => {
try {
const message = getMessage(request.params.id, request.user);
response.end(message);
} catch (e) {
response.status(403).end({ error: e.message });
}
});
First, we use our authMiddleware
; this makes sure that our request
has a user
object in our endpoint handlers.
Then, we define a function that is called for a GET request to our /messages/:id
endpoint. It uses the id
parameter and the user
object of our request
to load a message
with our business logic function.
When everything went right, we get the message
and can send it to the client. If not, we set the HTTP status to 403
(forbidden).
The getMessage
function handles the authorization with the help of the user
object. It doesn't care where it comes from.
Next, let us look at a GraphQL implementation, here created with Apollo Server.
expressApp.use(authMiddleware);
const { ApolloServer, gql } = require("apollo-server-express");
const typeDefs = gql`
type Query {
message(id: String!): String
}
`;
const resolvers = {
Query: {
message: (parent, args, request, info) =>
getMessage(args.id, request.user)
}
};
const server = new ApolloServer({ typeDefs, resolvers });
server.applyMiddleware({ app: expressApp });
The use of the middleware stays the same here. So no changes needed for authentication.
We then use the apollo-server-express
module to define a simple GraphQL API that only has a Query
type with a message
.
Then we define our resolver function, which looks almost the same as in the REST example.
Resolvers get four arguments, the third being the request
we already know from our REST implementation. Since our middleware used this object to attach the user
object, we can pass it on to our business logic as we did before.
The GraphQL engine parsed the id
parameter from our query and stored it into the second parameter, so only its location is different.
Our business logic function handles the authorization, the same as in the REST implementation.
The difference here is that GraphQL doesn't know about HTTP or its status codes. If it can process a query (even with empty results), it will just use 200
.
The error thrown by our business logic will be placed inside an errors
array in the response body and can be handled there in some way by the client.
The message
of our response JSON will be set to null
, this allows us to respond with partial data to the clients' requests. For example if the message would have been somewhere deep in the GraphQL response tree.
{
"data": {
"message": null
},
"errors": [
{
"message": "no permission",
...
}
]
}
What is Moesif? Moesif is the most advanced REST and GraphQL analytics platform used by over 2000 organizations to measure how your queries are performing and understand what your most loyal customers are doing with your APIs.
Handling in the Client
Now that we have talked about authentication and authorization in our API, we should also look at the client side of things.
Client Authentication
If we go for token-based authentication, which is currently the most prominent, we first need to acquire such a token. We can do this in two ways.
Auth Token Retrieval
- We create extra HTTP endpoints for signup and sign in
- We create GraphQL mutations for signup and sign in
Extra HTTP endpoints decouple the whole process of authentication from the GraphQL API. Like the middleware approach on the server-side did. This is especially nice if the service that does authentication is different from our API and we only need a token from it.
GraphQL mutations feel more integrated but require more special cases in our API, because we need to let users access them somehow without being authenticated.
In my opinion, the first way is less risky. We can focus on our core-competencies in our GraphQL API and stay flexible for the future.
Token Storage and Placement
The token needs to be sent to our API alongside with our GraphQL requests.
For this, we need to decide where to store the token and where on a request to place it.
There are two ways for storage, localStorage
and HTTP cookies.
When storing in localStorage
, we can send the token via HTTP headers. When we store it in cookies, we send it automatically with the cookies.
It's the same as with REST APIs, and both variants have their pros and cons. I won't go into detail, but you can read about it in our REST auth article.
Example with Apollo Client
Let's look at a simple example with Apollo Client, a UI-framework independent GraphQL client library written in JavaScript.
import { ApolloClient } from "apollo-client";
import { HttpLink } from "apollo-link-http";
import { ApolloLink, concat } from "apollo-link";
const httpLink = new HttpLink({ uri: "/graphql" });
const authMiddleware = new ApolloLink((operation, forward) => {
operation.setContext({
headers: {
authorization: localStorage.getItem("token") || null
}
});
return forward(operation);
});
const client = new ApolloClient({
link: concat(authMiddleware, httpLink)
});
Here we use the localStorage
/header variant to store and send the token.
Apollo client has a network layer called Apollo Link. This is used to make different protocols pluggable. A link is implemented as middleware, as we saw in the back-end in our Express examples.
In this example, we configure the HTTP middleware to our GraphQL API endpoint and then create a custom middleware/link for authentication.
Both get plugged into the ApolloClient
constructor, so they get executed for all GraphQL queries we send.
The custom authMiddleware
looks into localStorage
before every request and sets the authorization
HTTP header to the token it found.
Here we already see the pros of keeping the authentication out of GraphQL. We can do a simple fetch
call to an HTTP endpoint for signup or sign in and store the received token somewhere before we even start creating our GraphQL client objects.
This also works the other way around. If we want to sign out a user, we can throw away the GraphQL client object, without any need to mutate it in some way.
Client Authorization
If we set up the authentication correctly, the API now knows that we are who we say, but there is still the possibility that we want to query for data we aren't allowed to access.
On the client, like on the back-end, authorization is business logic, so we need to think about what to do in specific cases.
We have a UI, send one big GraphQL query to get all the data in one swoop, and now it comes back with some fields being null
and a few errors.
The Apollo Client library defines three error policies for this, and we can set one of them for every or all request.
These policies define behavior which we can also use in custom clients or when we fetch
directly without any particular GraphQL library.
- If any GraphQL error is present in the response, we consider it failed, and we throw it away, like when a network error occurred.
- We ignore all errors in the response and try to use the data as is.
- We try to use the GraphQL errors inside the response in tandem with the data that was returned to show the user the best we can and display the errors alongside so they know why some parts are missing.
Conclusion
Authentication and authorization are very important topics in API design, but as this article showed, once we understand the basics of authentication for HTTP APIs, we can reuse our skills on REST and GraphQL without much change.
It's also important to understand that authorization is business logic and often completely customized for a specific software product. This means it should be pushed to the edges of our code-base, so it can be found easily and doesn't repeat itself in many places.
Moesif is the most advanced GraphQL Analytics platform. Over 2000 organizations use Moesif to track what their most loyal customers do with their APIs. Learn More
Originally published at www.moesif.com
Top comments (0)