question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Authentication - Context creation failed:

See original GitHub issue

As i am developing a gql server backend using Apollo server (v2.4.8), i am in need to implement authentication for my gql endpoints.

i used jwt for this and followed the apollo server documentation to implement same.

https://www.apollographql.com/docs/apollo-server/security/authentication/

i did same like the example given in the documentation

context: ({ req }) => {
 // get the user token from the headers
 const token = req.headers.authentication || '';
  
 // 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) throw new AuthenticationError('you must be logged in to query this schema');  

 // add the user to the context
 return {
   user,
   models: {
     User: generateUserModel({ user }),
     ...
   }
 };
},

it worked and throwing error response to the client. but the problem is its not giving the same error format which is used expected by all the gql clients. my gql endpoints are used by iOS, Android and React client applications.

actual error format should be

{
    "errors": [
      {
        "message": "my custom message here",
        "extensions": {
          "code": "UNAUTHENTICATED",
          "exception": {
                     }
        }
      }
    ]
}

but the client receiving the auth error like below,

{
  "error": {
    "errors": [
      {
        "message": "Context creation failed: my custom message here",
        "extensions": {
          "code": "UNAUTHENTICATED",
          "exception": {
                     }
        }
      }
    ]
  }
}

so, me and the iOS developer were started blaming each other for unknown error format. he strictly said that he needs the error object as same as the model defined in his gql client (he uses apollo client for iOS swift https://www.apollographql.com/docs/ios/) and he started suspecting me that i am doing some custom error handling instead of the default error format by apollo server.

he shared me the below documentation as well. https://www.apollographql.com/docs/apollo-server/data/errors/

i gone through it and its same which i did with my gql backend code. so we don’t know what caused this error format changes?

in React client, i am storing the JWT token into the localStorage and validating the jwt token using jwt-decode npm module https://www.npmjs.com/package/jwt-decode . so i know that whether my jwt token is expired or not. if its expired, i will delete the token force the user to login again.

but my iOS app developer don’t have access to validate the jwt token and he fully depends on the backend for jwt validation and authentication but the backend give him unknown error format.

After spending some couple of hours with gql backend code, i did some work around and asked the iOS dev to retry the same. now the error is not coming, instead it broken our auth layer and he can able to get data without valid jwt token.

what i did was i tried to create an auth middle like we use with expressJs. i chosen addSchemaLevelResolveFunction to do this.

  • i suppressed the error in the context block. instead of throwing auth error, i returned null if no jwt token in the auth header or its invalid token
const getMe = req => {
  const {
    headers: { authorization },
    query,
  } = req;

  let token = '';

  if (authorization && authorization.split(' ')[0] === 'Bearer')
    token = authorization.split(' ')[1];
  else if (query && query.token) token = query.token;

  console.log({ token }, 'getMe');

  if (!token) {
    return {
      user: null,
      errorCode: null,
    };
  }

  try {
    const user = jwt.verify(token, process.env.JWT_SECRET);
    return {
      user,
      errorCode: null,
    };
  } catch (e) {
    const { name, message } = e;
    const errorCode = getJWTErrorCode(name) || message || 'Your session expired. Sign in again.';
    return {
      user: null,
      errorCode,
    };
  }
};

const server = new ApolloServer({
  schema,
  context: async ({ req, connection }) => {
    if (connection) return {};

    const { headers } = req;

    let me, authError;

    if (headers) {
      const { user, errorCode } = getMe(req);

      console.log({ user, errorCode }, 'context');
      me = user;
      authError = errorCode;
    }

    return {
      models: {
        user: models.user,
        blogPost: models.blogPost,
        notification: models.notification,
      },
      me,
      authError,
    };
  },
});

const getJWTErrorCode = name => {
  switch (name) {
    case 'TokenExpiredError':
      // token expired
      return '1001';
    case 'JsonWebTokenError':
      // jwt token malformed
      return '1002';
    case 'NotBeforeError':
      // jwt not active
      return '1003';
    default:
      return undefined;
  }
};

// addSchemaLevelResolveFunction  should be like below

const rootResolveFunction = (root, args, context, info) => {
  console.log('info', info.fieldName);
  const enableAuth = {
    // List of Queries to be protected
    users: true,
    profile: true,
    user: true,
    otps: true,
    updateProfile: true,
    createInviteCode: true,
    invideCodes: true,
    // List of Mutations to be protected
  };
  const isAuthRequired = enableAuth[info.fieldName];

  console.log(isAuthRequired, enableAuth[info.fieldName], 'rootResolveFunction');

  console.log({ context }, 'context value in root resolver');

  if (context && context.authError && context.authError != null)
    throw new AuthenticationError(context.authError);

  if (isAuthRequired && !context.me) throw new AuthenticationError('You are not authorized.');
};

addSchemaLevelResolveFunction(schema, rootResolveFunction);

i expectation was,

  • this addSchemaLevelResolveFunction will take care of jwt validation as well as safe guard my auth protected gql endpoints.

but the reality is,

  • it happen for only one time - initial request from the client. subsequent requests are crossed this validation boundary and reached the queries and mutation and return the data as well.

its very annoying and time consuming. my big disappointment is apollo documentation example itself not working well.

so, i changed my code as below and handled the auth validation in each and every queries and mutation as below

const enableAuth = {
  // List of Queries to be protected
  users: true,
  profile: true,
  user: true,
  otps: true,
  updateProfile: true,
  createInviteCode: true,
  invideCodes: true,
  // List of Mutations to be protected
};

const getJWTErrorCode = name => {
  switch (name) {
    case 'TokenExpiredError':
      // token expired
      return '1001';
    case 'JsonWebTokenError':
      // jwt token malformed
      return '1002';
    case 'NotBeforeError':
      // jwt not active
      return '1003';
    default:
      return undefined;
  }
};

const getMe = req => {
  const {
    headers: { authorization },
    query,
    body: { operationName },
  } = req;

  let token = '';

  const isAuthRequired = enableAuth[operationName];

  if (authorization && authorization.split(' ')[0] === 'Bearer')
    token = authorization.split(' ')[1];
  else if (query && query.token) token = query.token;

  console.log({ token }, 'getMe');

  if (!token) {
    return {
      user: null,
      errorCode: isAuthRequired ? 'JWT_TOKEN_MISSING' : null,
    };
  }

  try {
    const user = jwt.verify(token, process.env.JWT_SECRET);
    return {
      user,
      errorCode: null,
    };
  } catch (e) {
    const { name, message } = e;
    const errorCode = getJWTErrorCode(name) || message || 'Your session expired. Sign in again.';
    return {
      user: null,
      errorCode,
    };
  }
};

const server = new ApolloServer({
  schema,
  context: async ({ req, connection }) => {
    if (connection) return {};

    const { headers } = req;

    let me, authError;

    if (headers) {
      const { user, errorCode } = getMe(req);

      console.log({ user, errorCode }, 'context');
      me = user;
      authError = errorCode;
    }

    return {
      models: {
        user: models.user,
        blogPost: models.blogPost,
        notification: models.notification,
      },
      me,
      authError,
    };
  },
});

// inside each query and mutation, 

 if (context && context.authError && context.authError.length > 0)
        throw new AuthenticationError(context.authError);

now, getting the expected correct error format as below,

{
  "errors": [
    {
      "message": "1001",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "users"
      ],
      "extensions": {
        "code": "UNAUTHENTICATED",
        "exception": {
          "stacktrace": [
            "AuthenticationError: 1001",
            "    at users (/app/src/graphql/resolvers/user.js:36:15)",
            "    at field.resolve (/app/node_modules/graphql-extensions/dist/index.js:128:26)",
            "    at resolveFieldValueOrError (/app/node_modules/graphql/execution/execute.js:480:18)",
            "    at resolveField (/app/node_modules/graphql/execution/execute.js:447:16)",
            "    at executeFields (/app/node_modules/graphql/execution/execute.js:294:18)",
            "    at executeOperation (/app/node_modules/graphql/execution/execute.js:238:122)",
            "    at executeImpl (/app/node_modules/graphql/execution/execute.js:85:14)",
            "    at Object.execute (/app/node_modules/graphql/execution/execute.js:62:35)",
            "    at /app/node_modules/apollo-server-core/dist/requestPipeline.js:198:42",
            "    at Generator.next (<anonymous>)",
            "    at /app/node_modules/apollo-server-core/dist/requestPipeline.js:7:71",
            "    at new Promise (<anonymous>)",
            "    at __awaiter (/app/node_modules/apollo-server-core/dist/requestPipeline.js:3:12)",
            "    at execute (/app/node_modules/apollo-server-core/dist/requestPipeline.js:182:20)",
            "    at Object.<anonymous> (/app/node_modules/apollo-server-core/dist/requestPipeline.js:136:35)",
            "    at Generator.next (<anonymous>)"
          ]
        }
      }
    }
  ],
  "data": null
}

now the error format issue is resolved but i need to handle this in each and every queries and mutations to safeguard it.

why i am sharing this here is i came across issues like this in the apollo-server github and i want to help the others by sharing my hack to temporarily resolve the issue.

i am expecting global middleware like we have in expressJs to do these kind of authentication logics before reaching the queries and mutation and want to return the same error format which should be in expected format for all of the apollo clients like iOS, Android and web (Reactjs).

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

4reactions
nasr18commented, Dec 12, 2019

@Mohamed-Abubucker to overcome this issue, you can do any one from below

  1. use formatError,
  2. use apollo-resolvers.
0reactions
glassercommented, Mar 4, 2022

@jbadger Does https://github.com/apollographql/apollo-server/issues/6140 address the issue you’re seeing? This issue is pretty long and hard for me to understand exactly what it is getting at since it doesn’t include a minimal reproduction.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Context Creation Failed during Signin with React & Apollo
i ran in my ApolloClient's authLink... await AsyncStorage.removeItem("token");. then commented it out, n it worked again!
Read more >
How to handle errors that are from "context creation" ? : r/graphql
So my token has expired. And within the context I check the "token" from HTTP Header and throw an error when it's not...
Read more >
Authentication and authorization - Apollo GraphQL Docs
If no user exists or if lookup fails, the function throws an error and the corresponding operation is not executed. In resolvers. API-wide...
Read more >
Errors - The GraphQL Guide
Our piece of the message— malformed jwt in authorization header —is preceded by Context creation failed: , which is added by Apollo for...
Read more >
redwood/api-server - Context creation failed
After performing a login on my web app. I can see a request being made to .netlify/identity/token - which is 200 OK and...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found