graphql + apollo-server-express, how to handle auth error in express authMiddleware?
Asked Answered
S

2

7

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.

Serpentiform answered 2/8, 2018 at 2:53 Comment(3)
have you figured this out yet? I am facing this issue as well.Franky
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.Ulaulah
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. :/Tequilater
G
0

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.

Gardner answered 11/6, 2019 at 9:36 Comment(0)
D
0

The currently recommended approach (as of Apollo v4) is to handle your "middleware" logic in the context function provided to ApolloServer. You can create helper functions that take in the req so they operate like the old school express middlewares (with app.use).

From the Apollo Server documentation

interface MyContext {
  user: UserInterface;
}

const server = new ApolloServer<MyContext>({
  typeDefs,
  resolvers,
});

const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => {
    // get the user token from the headers
    const token = req.headers.authorization || '';

    // try to retrieve a user with the token
    const user = getUser(token);

    // optionally block the user
    // we could also check user roles/permissions here
    if (!user)
      // throwing a `GraphQLError` here allows us to specify an HTTP status code,
      // standard `Error`s will have a 500 status code by default
      throw new GraphQLError('User is not authenticated', {
        extensions: {
          code: 'UNAUTHENTICATED',
          http: { status: 401 },
        },
      });

    // add the user to the context
    return { user };
  },
});

console.log(`🚀 Server listening at: ${url}`);
Der answered 26/7, 2024 at 23:23 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.