This article was published on Wednesday, June 19, 2019 by Dotan Simha @ The Guild Blog
After a few years of working with GraphQL, as open-source developers and as infrastructure team in
large enterprises, we've learned some lessons about GraphQL, and how to authenticate and authorize
GraphQL API.Authentication and authorization should be simple, because for most cases, it's just a piece of code
that we wish to run before letting users access certain resources.In this article we'll go through all the different ways of implementing authentication and
authorization, the benefits and downsides to each one, and offer a solution that we believe is the
best philosophy to do so.## Authentication / Authorization?I found
good answer in StackOverflow
covers the main differences between authentication and authorization:In this article, I'll cover the difference between authentication and authorization with GraphQL
APIs, explain how to implement them with GraphQL server, and with the
GraphQL-Modules framework.We learned that a good implementation for GraphQL authentication has the following features
regarding authentication:* Your authentication implementation should eventually provide the currentUser
to the resolvers
- Your resolvers should not know about the authentication logic (separation between the business logic and the authentication logic)
- You wish to protect only parts of your GraphQL schema, and not all of it.
- You want to be able to authenticate parts of your schema, on a
field
level.And the following features regarding authorization:* You wish to protect some fields, according to custom rules. - You wish to run custom logics that protects parts of your GraphQL schema.
- Our custom rules should not be coupled to a specific resolver.## Where to Put Your Authentication?To understand why implementing authentication in GraphQL might be tricky, let's start with the
following chart:A request comes from the network, go through an HTTP server (for example,
express
) β That's #1 in the chart. Then it goes through GraphQL server, which builds acontext
, and then it runs your resolvers β #2. Then, you have your business logic, that you might want to separate from the resolvers, that's #3.Basically, you can implement GraphQL authentication on each part of this chart. But there are differences between those points that might effect the behaviour of your server, and might limit you later doing some things.Let's focus on the features requirements I mentioned before, and try to understand how the implementation selection might affect you.So let's take #1: If you implement your authentication on #1 (between the network request and the HTTP server), you probably does it with express middleware or something similar. The benefit in this is the separation between authentication logic and your business logic. But, you can't connect your data from the HTTP request to your GraphQL server, and it might be difficult later to get access to the currentUser. It also protects the entire GraphQL endpoint, and not only parts of it. So that's nice, but not perfect.Regarding #3: Implementing authentication as part of your resolver code or business logic code isn't a good idea. It usually means that your app code knows too much about the authentication, and it's probably coupled to it. It might to simpler to implement, but much more difficult to reuse later.And, #2: You can implement authentication between your HTTP server and your GraphQL server, by using the GraphQLcontext
. By implementing a customcontext
building function, you can access the network request and build yourcontext
object, and addcurrentUser
to it. Then, yourresolvers
getting called by the GraphQL engine.From our experience, we can tell that implementing authentication in this part of your GraphQL server gives you control over your authentication flow, if it's done right.In the next chapter we'll dive in, and see how easy it's to implement in with Apollo-Server.## Getting Started with AuthenticationWe'll start by implementing a simple authentication flow in GraphQL server (using Apollo-Server 2.0).I'm assuming that you already have your own favorite way to authenticate, whether it's with headers, cookies or any other way. In the point of view of the server, we expect to get a token, and we'll assume that it comes from the headers for the simplicity.Let's start by creating a simple Apollo-Server instance, with a very simple schema. We'll focus on exposing thecurrentUser
as a query field. ```ts import { ApolloServer } from 'apollo-server' import gql from 'graphql-tag'
const typeDefs = gql
type Query {
me: User
}
type User {
id: ID!
username: String!
}
const server = new ApolloServer({ typeDefs })
server.listen().then(({ url }) => {
console.log(π Server ready at ${url}
)
})
Now, let's implement `context` , and see if we can get a valid user out of the values we get there.
Our goal in this part is only to validate the token, and try to trade it for a user. If the token or
the user are not valid β we don't want to throw an exception here, because that way, our server will
not support public queries. So we're just trying to get a user and a token, and return them to
Apollo-Server, so it will use it as the context for our resolvers. If something went wrong with the
validation β we just return `null` for the `user` and `authToken`.
```ts filename="auth-helpers.ts"
export interface User {
_id: string
username: string
}
export async function tradeTokenForUser(token: string): Promise<User> {
// Here, use the `token` argument, check its validity, and return
// the user only if the token is valid.
// You can also use external auth libraries, such as jsaccounts / passport, and
// trigger its logic from here.
}
import { ApolloServer } from 'apollo-server'
import gql from 'graphql-tag'
import { tradeTokenForUser } from './auth-helpers'
const HEADER_NAME = 'authorization'
const typeDefs = gql`
type Query {
me: User
}
type User {
id: ID!
username: String!
}
`
const server = new ApolloServer({
typeDefs,
context: async ({ req }) => {
let authToken = null
let currentUser = null
try {
authToken = req.headers[HEADER_NAME]
if (authToken) {
currentUser = await tradeTokenForUser(authToken)
}
} catch (e) {
console.warn(`Unable to authenticate using auth token: ${authToken}`)
}
return {
authToken,
currentUser
}
}
})
server.listen().then(({ url }) => {
console.log(`π Server ready at ${url}`)
})
*Note that in a good GraphQL server implementation, the context
is built once per request, and
shouldn't change over time. So they only way to modify it is using the context
function that
builds it before the resolver getting called.*Now, we can finally implement the resolvers
for the server:
import { ApolloServer } from 'apollo-server'
import gql from 'graphql-tag'
import { tradeTokenForUser } from './auth-helpers'
const HEADER_NAME = 'authorization'
const typeDefs = gql`
type Query {
me: User
}
type User {
id: ID!
username: String!
}
`
const resolvers = {
Query: {
me: (root, args, context) => context.currentUser
},
User: {
id: user => user._id,
username: user => user.username
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
let authToken = null
let currentUser = null
try {
authToken = req.headers[HEADER_NAME]
if (authToken) {
currentUser = await tradeTokenForUser(authToken)
}
} catch (e) {
console.warn(`Unable to authenticate using auth token: ${authToken}`)
}
return {
authToken,
currentUser
}
}
})
server.listen().then(({ url }) => {
console.log(`π Server ready at ${url}`)
})
This is a naive implementation, that assumes that the token is valid, because otherwise the
context.currentUser
will be null
. That leads us to the next steps: implementing guards.To add guards to your resolvers, you can use a simple Middleware approach, and wrap your resolver
with a function that checks if context.user
is valid, and otherwise, throws an error. You are
getting full control over your authentication flow, because you can choose which resolvers to wrap.So let's implement a simple guard, and use it in our server. I also added a public part of the
schema (Query.serverTime
), that should be public, and not effected by the authentication flow. So
it's not wrapped with our guard.
``ts filename="authenticated-guard.ts"
Unauthenticated!`)
export const authenticated = next => (root, args, context, info) => {
if (!context.currentUser) {
throw new Error(
}
return next(root, args, context, info)
}
```ts
import { ApolloServer } from 'apollo-server'
import gql from 'graphql-tag'
import { tradeTokenForUser } from './auth-helpers'
const HEADER_NAME = 'authorization'
const typeDefs = gql`
type Query {
me: User
serverTime: String
}
type User {
id: ID!
username: String!
}
`
const resolvers = {
Query: {
me: authenticated((root, args, context) => context.currentUser),
serverTime: () => new Date()
},
User: {
id: user => user._id,
username: user => user.username
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
let authToken = null
let currentUser = null
try {
authToken = req.headers[HEADER_NAME]
if (authToken) {
currentUser = await tradeTokenForUser(authToken)
}
} catch (e) {
console.warn(`Unable to authenticate using auth token: ${authToken}`)
}
return {
authToken,
currentUser
}
}
})
server.listen().then(({ url }) => {
console.log(`π Server ready at ${url}`)
})
Great! So now our server is authenticated and we can get the currentUser
in our resolvers. It also
protects only the part of the GraphQL schema that we wanted to protect, and the authentication flow
is separated from the resolvers, so each resolver does only what it needs.## Getting Started with AuthorizationNow that we understand the concept of authentication in GraphQL servers, we can also implement
authorization.Implementing Authorization is no different from implementing the authenticated
guard we did
before. It's just another guard, or resolver wrapper we can use.Let's add type Article
to the schema, and allow creating article only to users with role
set to
EDITOR
. It's that simple:
import { ApolloServer } from 'apollo-server'
import gql from 'graphql-tag'
import { tradeTokenForUser } from './auth-helpers'
import { authenticated } from './authenticated-guard'
import { validateRole } from './validate-role'
const HEADER_NAME = 'authorization'
const typeDefs = gql`
type Query {
me: User
serverTime: String
}
type User {
id: ID!
username: String!
}
type Article {
id: ID!
title: String!
content: String!
}
type Mutation {
publishArticle(title: String!, content: String!): Article!
}
`
const resolvers = {
Query: {
me: authenticated((root, args, context) => context.currentUser),
serverTime: () => new Date()
},
Mutation: {
publishArticle: authenticated(
validateRole('EDITOR')((root, { title, content }, context) =>
createNewArticle(title, content, context.currentUser)
)
)
},
User: {
id: user => user._id,
username: user => user.username
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
async context({ req }) {
let authToken = null
let currentUser = null
try {
authToken = req.headers[HEADER_NAME]
if (authToken) {
currentUser = await tradeTokenForUser(authToken)
}
} catch (e) {
console.warn(`Unable to authenticate using auth token: ${authToken}`)
}
return {
authToken,
currentUser
}
}
})
server.listen().then(({ url }) => {
console.log(`π Server ready at ${url}`)
})
``ts filename="validate-role.ts"
Unauthorized!`)
export const validateRole = role => next => (root, args, context, info) => {
if (context.currentUser.role !== role) {
throw new Error(
}
return next(root, args, context, info)
}
## Next-Level Implementation with GraphQL-Modules![GraphQL Modules](https://the-guild.dev/blog-assets/graphql-modules-auth/graphql-modules.png "GraphQL Modules")If you wish to take your GraphQL server to the next level, and build a scalable, testable and
readable server, I recommend to give [GraphQL-Modules](https://graphql-modules.com) a try.With GraphQL-Modules, you can separate your schema to smaller pieces, and creates modules that are
in-charge of small parts of your code. It also provides a `resolversComposition` feature, that acts
like a powerful middleware mechanism, that let you write your modules without knowing about the
authentication flow, and just wrap your resolvers with guards in the app-level, so modules are
independent and do only what they need, and the app that uses these modules decide which resolvers
to authenticate.It also super powerful, because you can implement your GraphQL-Module as standalone module, use it
in multiple applications, and then apply different authentication/authorization rules in each app.To implement the same example above with GraphQL-Modules, you can create a module called `auth` and
move the `context` building logic to there. Then, create another module called `common` and the
general parts, and articles to manage the articles features.### This Way β Each Module Is in Charge of a Different Feature of Our App
```ts filename="articles-module.ts"
import { GraphQLModule } from '@graphql-modules/core'
export const articlesModule = new GraphQLModule({
name: 'articles',
// authModule must be imported, because this module uses currentUser in the context
imports: [authModule],
typeDefs: gql`
type Article {
id: ID!
title: String!
content: String!
}
type Mutation {
publishArticle(title: String!, content: String!): Article!
}
`,
resolvers: {
Mutation: {
publishArticle: (root, { title, content }, { currentUser }) =>
createNewArticle(title, content, currentUser)
}
}
})
```ts filename="auth-module.ts"
import { GraphQLModule } from '@graphql-modules/core'
import { tradeTokenForUser } from './auth-helpers'
const HEADER_NAME = 'authorization'
export const authModule = new GraphQLModule({
name: 'auth',
typeDefs: gql
,
type Query {
me: User
}
type User {
id: ID!
username: String!
}
resolvers: {
Query: {
me: (root, args, { currentUser }) => currentUser
},
User: {
id: user => user._id,
username: user => user.username
}
},
async contextBuilder({ req }) {
let authToken = null
let currentUser = null
try {
authToken = req.headers[HEADER_NAME]
if (authToken) {
currentUser = await tradeTokenForUser(authToken)
}
} catch {
console.warn(`Unable to authenticate using auth token: ${authToken}`)
}
return {
authToken,
currentUser
}
}
})
```ts filename="common-module.ts"
import { GraphQLModule } from '@graphql-modules/core'
export const commonModule = new GraphQLModule({
name: 'common',
typeDefs: gql`
type Query {
serverTime: String
}
`,
resolvers: {
Query: {
serverTime: () => new Date()
}
}
})
```ts filename="resolvers-composition.ts"
import { authenticated } from './authenticated-guard'
import { validateRole } from './validate-role'
export const resolversComposition = {
'Query.me': [authenticated],
'Mutation.publishArticle': [authenticated, validateRole]
}
```ts filename="server.ts"
import { ApolloServer } from 'apollo-server'
import { resolversComposition } from './resolvers-composition'
const app = new GraphQLModule({
name: 'app',
imports: [commonModule, authModule, articleModule],
resolversComposition
})
const { schema, context } = app
const server = new ApolloServer({
schema,
context
})
server.listen().then(({ url }) => {
console.log(`π Server ready at ${url}`)
})
The Future of Authentication with GraphQL-Modules and GraphQL @directivesIf you wish to get rid of the resolversComposition
wrapping, and use a more declarative way, you
can use GraphQL directives to decorate your schema and put the authentication/authorization there.To get started, add the following @directives to your auth
module's schema:
directive @auth on FIELD
directive @protect(role: String) on FIELD
Then, you can easily alter your resolversComposition
into a function that accepts GraphQLSchema
, and then check using your schema object, which resolves require which logic, and do a mapping:
import { getFieldsWithDirectives } from '@graphql-modules/utils'
import { authenticated } from './authenticated-guard'
import { validateRole } from './validate-role'
const DIRECTIVE_TO_GUARD = {
auth: () => authenticated,
protect: ({ role }) => validateRole(role)
}
export const resolversComposition = ({ typeDefs }) => {
const fieldsAndTypeToDirectivesMap = getFieldsWithDirectives(typeDefs)
for (const fieldPath in fieldsAndTypeToDirectivesMap) {
const directives = fieldsAndTypeToDirectivesMap[fieldPath]
if (directives.length > 0) {
result[fieldPath] = directives
.map(directive => {
if (DIRECTIVE_TO_GUARD[directive.name]) {
const mapperFn = DIRECTIVE_TO_GUARD[directive.name]
return mapperFn(directive.args)
}
return null
})
.filter(Boolean)
}
}
return result
}
The, you add the directives to your schema declaration:
type Mutation {
publishArticle(title: String!, content: String!): Article! @auth @protect(role: "EDITOR")
}
type Query {
me: User @auth
}
What's Next?Another thing we are working on, is having some of your repeated authentication and authorization
logic installable through npm!We are working with the creators of the js-accounts
libraries to create a GraphQL Modules module out
of their awesome packages, so you could just add it from npm and extend your server and schema.You can read this blog post to learn to use this library with GraphQL-Modules in a few steps;/blog/accountsjs-graphql-modules**Authentication and authorization is a fundamental part of your GraphQL server.It should be easy and secure to implement and understand how to do that in order to help grow the
GraphQL communityIn this article we wanted to give you a complete overview of authentication and authorization in the
GraphQL ecosystem and give you the tools and the knowledge to implement it on your GraphQL servers.Please try all those different approaches and let us know your feedback about the best way for you
and if we can improve it even further.### All Posts about GraphQL Modules GraphQL Modules β Feature based GraphQL Modules at scale
- Why is True Modular Encapsulation So Important in Large-Scale GraphQL Projects?
- Why did we implement our own Dependency Injection library for GraphQL-Modules?
- Scoped Providers in GraphQL-Modules Dependency Injection
- Writing a GraphQL TypeScript project w/ GraphQL-Modules and GraphQL-Code-Generator
- Authentication and Authorization in GraphQL (and how GraphQL-Modules can help)
- Authentication with AccountsJS & GraphQL Modules
- Manage Circular Imports Hell with GraphQL-Modules
Top comments (0)