6
votes

I can't figure out how to handle auth error in my authMiddleware function.

Here is my authMiddleware function with traditional express way error handling.

const jwt = require('jsonwebtoken');
const { appConfig } = require('../config');

function authMiddleware(req, res, next) {
  let token;
  const parts = req.headers.authorization.split(' ');

  if (parts.length === 2) {
    const schema = parts[0];
    const credentials = parts[1];

    if (/^Bearer$/i.test(schema)) {
      token = credentials;
    } else {
      // throw new Error();
      next(new Error('credentials_bad_scheme: Format is Authorization: Bearer [token]'));
    }
  }

  try {
    const { user } = jwt.verify(token, appConfig.JWT_SCERET);
    req.user = user;
  } catch (error) {
    // console.log(error);
    next(error);
  }
  next();
}

exports.authMiddleware = authMiddleware;

But with apollo-server-express and graphql system. The error passed into next function does not work fine. Which means it seems the express error handling way is not working any more when use graphql tool stack.

The error in authMiddleware will not pass below express error handling middleware

app.use((err, req, res) => {
  console.log('error handler: ', err);
});

If I use return res.status(401).json({code: 1001, msg: 'Authorization failed'}) or throw new Error('xxx') in catch when auth failed. The request will stop here forever which means will never go down to graphqlExpressHandler. In order to let request go down to graphqlExpressHandler, only thing I can do for the errors is to use console.log to print them.

And there is no way to use express-jwt unless method or credentialsRequired property. Because when use graphql, there is only one route named '/graphql'. So, you can't unless /graphql route

One way to solve this is: make restful api for auth and handle it in traditonal way. Make graphql api for data query.

1
have you figured this out yet? I am facing this issue as well. - forJ
You could abstract your authentication logic presented in your question, in a seperate function that just takes a header. Then in your "normal" express routes you use middleware to run the auth code, but in Apollo you use the context creation to call you function. - DauleDK
Had the same issue, using an Express middleware for auth, the rest of the API running in the Apollo Server. So errors outside the server aren't caught by the regular Apollo error handling. :/ - bombillazo

1 Answers

0
votes

Late answer, but may help someone facing the same issue.

Here is how we solved it:

graphql + apollo-server-express expose only the /graphql route, so the easy and good way is to expose authentication endpoint as a graphql mutation, and do token validation (what your authMiddleware does) in the context function passed to ApolloServer instance.

Example:

  1. Define token mutation.
// graphql.ts
import { gql } from 'apollo-server-express';
import AuthnHandler from './handlers/authn_handler';

export const typeDefs = gql`
  type Mutation {
    token(username: String, password: String): String
  }
`

const authnHandler = new AuthnHandler();

export const resolvers = {
  Mutation: {
    token: authnHandler.tokenResolver
  }
};
  1. Define token mutation resolver that validates credentials and issues token.
// handlers/authn_handler.ts
import { AuthenticationError } from 'apollo-server-express';

export default class AuthnHandler {
  public async tokenResolver(parent: any, args: any, context: any, info: any): Promise<any> {
    const username = args.username;
    const password = args.password;
    // pseudo-code here, replace with your token issuing implementation.
    // if credentials are valid, return Promise.resolve(token);
    // else throw new AuthenticationError('Invalid credentials.');
  }
}
  1. Define context function that validates token in authorization header (what your authMiddleware function does).
// server.ts
import express from 'express';
import { ApolloServer, ApolloServerExpressConfig } from 'apollo-server-express';
import { typeDefs, resolvers } from './graphql';
import { authMiddleware } from './auth_middleware';

const expressApp = express();

const apolloServer = new ApolloServer({
  typeDefs,
  resolvers,
  context: authMiddleware
} as ApolloServerExpressConfig);

apolloServer.applyMiddleware({ app: expressApp });

expressApp.listen(3000, () => {
  console.log('server listening on port 3000');
});

Your authMiddleware function signature changes as per the context function requirement, and in this case it returns the request object itself when success.

// auth_middleware.ts
const jwt = require('jsonwebtoken');
const { appConfig } = require('../config');

function authMiddleware({ req }) {
  let token;
  const parts = req.headers.authorization.split(' ');

  if (parts.length === 2) {
    const schema = parts[0];
    const credentials = parts[1];

    if (/^Bearer$/i.test(schema)) {
      token = credentials;
    } else {
      throw new Error();
    }
  }

  try {
    const { user } = jwt.verify(token, appConfig.JWT_SCERET);
    req.user = user;
  } catch (error) {
    throw new Error();
  }
  return { req };
}

exports.authMiddleware = authMiddleware;

The authentication section in the apollo-server documentation provides a detailed explanation of this way of implementation.