Authentication - Context creation failed:
See original GitHub issueAs 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:
- Created 4 years ago
- Comments:5 (2 by maintainers)
Top GitHub Comments
@Mohamed-Abubucker to overcome this issue, you can do any one from below
formatError
,apollo-resolvers
.@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.