meta pixel

JWT authentication using @apollo/gateway


    Posted by Leonardo Habitz

    on Apr 29th 2025


Apollo

Apollo is a platform that exposes a bunch of tools to work with graphql such as apollo-server and apollo-gateway. I’m going to show you how to design your microservices architecture with public and private mutations using the gateway engine provided by the apollo community.


Apollo gateway

Apollo gateway is an amazing tool that helps us to have a single entry point for our microservices.

And why do you need it? When you split your services into different API’s you have several addresses available in your cluster, each address corresponding to one microservice. For instance: http://localhost:3000, http://localhost:4000, etc.

The problem is that all the clients will have to store all of these urls to make the requests to the right services, that’s awful! This approach has so many problems, you can get more info about that here.

So, there’s the apollo-gateway! It’s the apollo solution for graphql microservices running in the apollo-server tool.


Apollo federation

Another tool that I used in this project is the apollo federation, it provides a helpful ability to put all the microservices together into a unique graph.

A unique graph, or single data graph, it’s a nice to have once you have (and you will) queries and mutations which needs more than one service to been called. An example is querying by a specific user and his products, assuming that you had split the user and products queries in two different services, you need a single graph to mix the information into one source.


Show us the code!

So, Let’s get started by creating the auth service, this API will have only one mutation called login and will be responsible for generating the jwt token.

package.json:

{

"name": "auth",

"scripts": {

"start": "node index.js"

},

"dependencies": {

"@apollo/federation": "^0.9.4",

"apollo-server": "^2.9.3",

"graphql": "^14.4.2",

"jsonwebtoken": "^8.5.1"

}

}

index.js:

const { ApolloServer, gql } = require('apollo-server')

const { buildFederatedSchema } = require('@apollo/federation')

const login = require('./login')

const mock = () => { }

const typeDefs = gql`

extend type Query {

mock: String

}

type LoginResponse {

token: String

}

input LoginInput {

email: String!

password: String!

}

type Mutation {

login(input: LoginInput): LoginResponse!

}

`

const resolvers = {

Query: { mock },

Mutation: { login }

}

const server = new ApolloServer({

schema: buildFederatedSchema([{ resolvers, typeDefs }])

})

server.listen({ port: 3000, cors: { origin: '*' } })

Note that the typeDefs has a mocked query, that’s because an error is displayed when a service has no query.


login.js

const { sign } = require('jsonwebtoken')

const login = (_, { input }) => {

if (input.email != 'admin' || input.password != '123') {

throw new Error('Invalid credentials')

}

return {

token: sign({ userId: 1 }, 'privateKey', { expiresIn: '1h' })

}

}

module.exports = login

Here the token is created using a securityKey (privateKey), containing the user and expires info. The token will be something like this:

‘eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTU3MjI2MzI4OSwiZXhwIjoxNTcyMjY2ODg5fQ.DabTdm3tImWD3ekboYyrXxPL4ZefL4og2Xpg9mR8FJo’


The products API.

package.json:


{

"name": "products",

"scripts": {

"start": "node index.js"

},

"dependencies": {

"@apollo/federation": "^0.9.4",

"apollo-server": "^2.9.3",

"graphql": "^14.4.2"

}

}

index.js

const { ApolloServer, gql } = require('apollo-server')

const { buildFederatedSchema } = require('@apollo/federation')

const products = (_, __, { userId }) => [{

id: 1,

userId,

name: 'Shoes'

}, {

id: 2,

userId,

name: 'Jacket'

}]

const typeDefs = gql`

type Product {

id: ID

userId: ID

name: String

}

extend type Query {

products: [Product]

}

`

const resolvers = {

Query: { products }

}

const server = new ApolloServer({

schema: buildFederatedSchema([{ resolvers, typeDefs }]),

context: ({ req }) => ({ userId: req.headers['user-id'] })

})

server.listen({ port: 4000, cors: { origin: '*' } })

In the example above we’re receiving the userId through the request headers and putting it into the context to show you how to access it from the resolvers.


And then the gateway

package.json


{

"name": "gateway",

"scripts": {

"start": "node index.js"

},

"dependencies": {

"@apollo/gateway": "^0.10.4",

"apollo-server": "^2.9.3",

"graphql": "^14.4.2",

"jsonwebtoken": "^8.5.1"

}

}

handleAuth.js

const { verify } = require('jsonwebtoken')


const PUBLIC_ACTIONS = ["login"]

const actionIsPublic = ({ query }) => (

PUBLIC_ACTIONS.some(action => query.includes(action))

)

const isIntrospectionQuery = ({ operationName }) => (

operationName === 'IntrospectionQuery'

)

const shouldAuthenticate = body => (

!isIntrospectionQuery(body) && !actionIsPublic(body)

)

const handleAuth = ({ req }) => {

if (shouldAuthenticate(req.body)) {

const decoded = verify(req.headers.authorization, 'privateKey')

return { userId: decoded.userId }

}

}

module.exports = handleAuth

Here we’re skipping the public mutations and queries from the auth validation (in this case just the login one) and also preventing the Introspection query to be validated. Then we verify the token received by the authorization header (using the same securityKey as shown before at the auth service) and put the userId extracted from the token into the gateway context. If the token is expired or invalid an error will be thrown

index.js

const { ApolloServer } = require('apollo-server')

const { ApolloGateway, RemoteGraphQLDataSource } = require('@apollo/gateway')

const handleAuth = require('./handleAuth')

const server = new ApolloServer({

gateway: new ApolloGateway({

serviceList: [

{ name: 'auth', url: 'http://localhost:3000' },

{ name: 'products', url: 'http://localhost:4000' }

],

buildService({ name, url }) {

return new RemoteGraphQLDataSource({

url,

willSendRequest({ request, context }) {

request.http.headers.set('user-id', context.userId)

}

})

}

}),

context: handleAuth,

subscriptions: false

})

const serverConfig = { port: 8080, cors: { origin: '*' } }

server.listen(serverConfig).then(({ url }) => {

console.log(`🚀 Server ready at ${url}`)

})

Now we just call the handleAuth function and then get the userId from the context to pass along to services by headers, that’s how the products service has access to the userId decoded via the request headers. This process happens in every request.


Further reading

If you are interested in studying how to configure your own gateway check the docs here. And here are the apollo-federation docs. Also, go to my github’s repo to see the full example.

Leonardo Habitz
Leonardo HabitzTechnology Expert