Authentication service with Apollo Gateway

2.3k Views Asked by At

I am developing a microservices-based application using Apollo Gateway. Each service is written in Node.js and uses Graphql to build a federated schema. I am also using Mongoose to interact with a MongoDB database shared between the services. The main goal of the development of this app is to learn and gain experience using tools and technologies new to me as Graphql, microservices, and Node.js.

I have a question regarding authentication. I decided to use JWT based authentication with additional database-stored sessions for each user. In this way, I can monitor active sessions for each user and revoke access by disabling the session associated with the token. All of this is managed by the Auth service which is responsible for authenticating, creating new users and login/logout functions. The Auth service exposes one REST endpoint to verify the jwt token as follows.

...
app.post('verify', async (req, res, next) => {
  const token = req.body.jwt;

  if(!token) {
    res.status(403).send({ error: 'No token provided.' });
  }

  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const user = await User.findOne({ _id: decoded.sub});

  if (!user) {
    res.status(401).send({ error: 'No user found.' });
  }

  const session = await Session.findOne({
    '_id': {
      $in: user.sessions
    }
  });
  if(!session) {
    res.status(401).send({ error: 'Session not found or expired.' });
  }
  if(!session.valid) {
    res.status(401).send({ error: 'Session not valid.' });
  }

   res.send({
    userId: user.id,
    scopes: user.scopes
  });
}

const server = new ApolloServer({
  schema: buildFederatedSchema([
    {
      typeDefs,
      resolvers
    }
  ]),
  context: ({ req }) => {
    return {
      // headers
      userId: req.get['user-id'] || 0,
      scopes: req.get['user-scopes'] ? req.get['user-scopes'].split(',') : [],
      // Mongoose models
      models: {
        User,
        Session
      }
    }
  }
});

server.applyMiddleware({ app, cors: false });
...

My API gateway is based on Apollo Gateway to build the federated schema. Authentication is verified by the Auth service and shared with every other service via request headers set by the gateway.

...
// Set authenticated user id in request for other services
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
  willSendRequest({ request, context }) {
    // pass the user's id from the context to underlying services
    // as a header called `user-id`
    request.http.headers.set('user-id', context.userId);
    request.http.headers.set('user-scopes', context.scopes.join(','));
  }
}

const gateway = new ApolloGateway({
  serviceList: [
    { name: 'auth', url: 'https://auth:4000' }
  ],
  buildService: ({ name, url }) => {
    return AuthenticatedDataSource({ url });
  }
});

// Apollo server middleware - last applied
const server = new ApolloServer({
  gateway,
  // not supported
  subscriptions: false,
  context: async ({ req }) => {
    try {
      // Send auth query to Auth service REST api
      const response = await axios.post('https://auth:4000/verify', {
        jwt: req.cookies['plottwist_login']
      });
      // save auth data in context
      return {
        userId: response.data.userId,
        scopes: response.data.scopes
      }
    } catch(e) {
       // deal with error
    }
  }
});

server.applyMiddleware({ app, path, cors: false });
...

This way the flow is the following:

  • API gateway receives the Graphql query request from the client.
  • API gateway queries the Auth service to authenticate the user using the only REST endpoint offered by the auth service (copying the token cookie from the received request).
  • Auth service authenticates the user and sends back data.
  • Gateway receives response, creates additional request headers and proceeds by managing the original Graphql query.

This comes with the cost of an extra call that the gateway makes before managing each Graphql query coming from the client. I wonder if this is a viable option or there is some major flaws in my reasoning.

0

There are 0 best solutions below