I'm wondering if there's a way to allow introspection queries only when a valid authorization header is passed.
I have introspection disabled outside development, but our client app needs to fetch the schema to be used in code generation for the iOS Apollo client as described here:
https://www.apollographql.com/docs/ios/downloading-schema.html
Currently it looks like I can only enable or disable configuration via a boolean introspection option passed in the config to the ApolloServer constructor.
I'd like to allow the client developer access to that introspection query if they include a valid internal token in the auth header. Is that possible?
It is not possible at the moment but I'm also looking for something similar.
Ideally introspection should be changed to be a bool or function that receives the request ascontextand returnstrue/false`.
@stephenhandley you could use formatResponse
@ftatzky I don't follow. I'm familiar with formatResponse but as I understand it that gets called after the request cycle, and if intrsopectionis false, it'll never reach that point. Maybe I'm missing something though, could you please elaborate?
@stephenhandley Sorry for the late response. Well formatResponse receives the response and the context. You could enable introspection by default but only return a valid response if a user successfully authenticated. Code could look something like this:
const apolloServer = new ApolloServer({
...
introspection: true,
context({ req }) {
const isValidAuth = checkAuthorization(req.headers.authorization)
return { isValidAuth }
},
formatResponse(response, { context }) {
if (context.isValidAuth) {
return response
}
return {}
},
...
})
@ftatzky ok, understood. the issue there is then that we have the performance overhead of introspection on every session query, regardless of whether they are an internal user (i.e for whom introspection would be appropriate)
[..] the issue there is then that we have the performance overhead of introspection on every session query
And the reverse is true too. We execute the query even if we can know before that it will fail.
Note: In graphql-yoga you can make validation rules that are aware of express' req object like this.
I really like @the-noob's suggestion!
It is not possible at the moment but I'm also looking for something similar.
Ideallyintrospectionshould be changed to be aboolorfunction that receives the request ascontextand returnstrue/false`.
Hey @stephenhandley, this can be accomplished via the request pipeline. The docs aren't published yet, but you can take a look here: https://github.com/apollographql/apollo-server/pull/2008
Does anybody know if this is possible today?
@richardscarrott – As a matter of fact, yes, this is absolutely possible by implementing an ApolloPlugin. Please note the remark from @trevor-scheer:
_[...]_ this can be accomplished via the request pipeline. The docs aren't published yet, but you can take a look here: #2008
There is helpful Q&A in the ongoing conversation and what's being a [WIP] here is "just" the actual documentation. The respective features do already live in ApolloServer.
You can see the interface that's being implemented by existing plugins in this file. You can pass plugins to the constructor of ApolloServer using its option plugins which expects an array: ApolloServerPlugin[]. You may also have a look at the deployed preview of the docs 📘
Try it out, it's at least pretty straight-forward to play around with, I think... 🔝
I hope these hints might help you out.
@nether-cat Would this be a valid way of protecting JUST the introspection query? I still want certain unauthorized queries to pass, like a login.
This plugin is currently checking for 3 things:
__schema.__type.__schema or __type is found in the query.Am I missing any obvious loopholes? Is there a better way to do this now?
NOTE: Invalid authorization headers may pass this check but are caught in my context middleware, where if there is an authorization header, it is validated.
const secureIntrospection = {
requestDidStart: ({ request, context }) => {
if (
(request.query.includes('__schema') ||
request.query.includes('__type')) &&
!context.req.get('authorization')
) {
throw new AuthenticationError('GraphQL introspection not authorized!');
}
},
};
const graphQLServer = new ApolloServer({
schema: schemaWithMiddleware,
context: contextMiddleware,
engine: { apiKey: CONFIG.ENGINE_API_KEY },
subscriptions: { path: '/' },
plugins: [secureIntrospection],
introspection: true,
// Development only
playground: CONFIG.IS_DEVELOPMENT,
debug: CONFIG.IS_DEVELOPMENT,
tracing: CONFIG.IS_DEVELOPMENT,
});
@nether-cat Would this be a valid way of protecting JUST the introspection query? I still want certain unauthorized queries to pass, like a
login.This plugin is currently checking for 3 things:
- If the query includes
__schema.- If the query includes
__type.- Requires an authorization header if
__schemaor__typeis found in the query.Am I missing any obvious loopholes? Is there a better way to do this now?
NOTE: Invalid authorization headers may pass this check but are caught in my context middleware, where if there is an authorization header, it is validated.
const secureIntrospection = { requestDidStart: ({ request, context }) => { if ( (request.query.includes('__schema') || request.query.includes('__type')) && !context.req.get('authorization') ) { throw new AuthenticationError('GraphQL introspection not authorized!'); } }, }; const graphQLServer = new ApolloServer({ schema: schemaWithMiddleware, context: contextMiddleware, engine: { apiKey: CONFIG.ENGINE_API_KEY }, subscriptions: { path: '/' }, plugins: [secureIntrospection], introspection: true, // Development only playground: CONFIG.IS_DEVELOPMENT, debug: CONFIG.IS_DEVELOPMENT, tracing: CONFIG.IS_DEVELOPMENT, });
@danielmahon Thanks for this example!
We used something like this in our codebase and released it as a package for usage across our products; referenced this comment in the package.
If anyone is interested:-
https://github.com/ClearTax/apollo-server-plugin-introspection-auth
We wanted only authenticated backend clients to be able to introspect in production, and the apollo-server-plugin-introspection-auth plugin doesn't really check what we need, so we wrote our own basic plugin for this like @danielmahon mentioned above.
Note though that using request.query.includes('__schema') || request.query.includes('__type')) would also reject any queries asking for __typename like those that Apollo Client makes, so better to use a regex with word breaks like the apollo-server-plugin-introspection-auth plugin does:
const INTROSPECTION_QUERY_REGEX = /\b(__schema|__type)\b/
const restrictIntrospectionPlugin = {
requestDidStart: ({ request, context }) => {
if (
INTROSPECTION_QUERY_REGEX.test(request.query)
) {
let isIntrospectionAuthorized = process.env.NODE_ENV === 'production' ? isBackendClient(context) : true
if (!isIntrospectionAuthorized) {
throw new AuthenticationError(
'GraphQL introspection not authorized!'
)
}
}
}
}
module.exports = { restrictIntrospectionPlugin }
I noticed a lot of the suggestions here use approaches like regex which could be easily bypassed - and in some cases introduce a security vulnerability.
# Fake introspection query to bypass auth
query IntrospectionQuery {
__schema {
__typename
}
}
# Some mutation that no longer requires auth
mutation MaliciousMutation {
deleteThing(id: 1234)
}
I've made a gist here which checks that the query is actually an introspection query and not just a query containing introspection.
@andyrichardson approach is great - but could be better suited to pair with the PluginDefinition callback for didResolveOperation which has the advantage of not having to scan or parse all documents twice. Reducing the runtime overhead of the check.
Cheers @tbrannam yeah you're right!
I guess my main point is that the suggestions prior could lead to security vulnerabilities
Most helpful comment
It is not possible at the moment but I'm also looking for something similar.
Ideally
introspectionshould be changed to be aboolorfunction that receives the request ascontextand returnstrue/false`.