Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Requiring auth header for introspection queries #1933

Closed
stephenhandley opened this issue Nov 7, 2018 · 23 comments
Closed

Requiring auth header for introspection queries #1933

stephenhandley opened this issue Nov 7, 2018 · 23 comments
Labels
⛲️ feature New addition or enhancement to existing solutions

Comments

@stephenhandley
Copy link

stephenhandley commented Nov 7, 2018

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?

@the-noob
Copy link

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 as contextand returnstrue/false`.

@ftatzky
Copy link

ftatzky commented Dec 6, 2018

@stephenhandley you could use formatResponse

@stephenhandley
Copy link
Author

@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?

@ftatzky
Copy link

ftatzky commented Dec 11, 2018

@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 {}
  },
  ...
})

@stephenhandley
Copy link
Author

@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)

@KATT
Copy link
Contributor

KATT commented Jan 4, 2019

[..] 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.

@nether-cat
Copy link

I really like @the-noob's suggestion!

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 as contextand returnstrue/false`.

@trevor-scheer trevor-scheer added the ⛲️ feature New addition or enhancement to existing solutions label Jul 8, 2019
@trevor-scheer
Copy link
Member

Hey @stephenhandley, this can be accomplished via the request pipeline. The docs aren't published yet, but you can take a look here: #2008

@trevor-scheer trevor-scheer added the 🚧👷‍♀️👷‍♂️🚧 in triage Issue currently being triaged label Jul 8, 2019
@abernix abernix removed 🚧👷‍♀️👷‍♂️🚧 in triage Issue currently being triaged labels Jul 9, 2019
@richardscarrott
Copy link

Does anybody know if this is possible today?

@nether-cat
Copy link

@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.

@danielmahon
Copy link

danielmahon commented Jan 30, 2020

@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:

  1. If the query includes __schema.
  2. If the query includes __type.
  3. Requires an authorization header if __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,
});

@rheaditi
Copy link

@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:

  1. If the query includes __schema.
  2. If the query includes __type.
  3. Requires an authorization header if __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,
});

@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

@scherroman
Copy link

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 }

@andyrichardson
Copy link

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.

@tbrannam
Copy link

@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.

@andyrichardson
Copy link

Cheers @tbrannam yeah you're right!

I guess my main point is that the suggestions prior could lead to security vulnerabilities

@elier-lg
Copy link

@trevor-scheer
I have been following up the question posted by @stephenhandley since I have currently the same issue. I read the post on #2008

Buts still not able to understand how to avoid that the Introspection call made from Gateway to federated service to get broken for not having Auth Header.

The only way I can bypass that issue is by adding a hardcoded Auth header like:

const gateway = new ApolloGateway({
  serviceList: config.serviceList,
  introspectionHeaders: (p: any) => 

> {

      return { Authorization:  'Bearer someValid token  };  // <------- Hardcoding here
  },
  
 buildService: ({ url }) => {
    return new RemoteGraphQLDataSource({
      url,
      willSendRequest: ({ request, context }: { request: GraphQLRequest; context: any }) => {
        request?.http?.headers.set('Authorization', context.auth);
      }
    });
  },
});

const server = new ApolloServer({
  context: {auth: 'some Valid token'} ,
  gateway
});

And this is because during first time call to federated service ('Introspection call') the context is received in willSendRequest params is empty.

But this is not a valid solution since the hardcoded token expires and then the pipeline gets broken again and throws error: _An error occurred during Apollo ..... 400: Bad Request.

@glasser
Copy link
Member

glasser commented Sep 22, 2021

It would be nice if the same mechanism that we use to implement introspection: false could be used here. That's a validation rule passed to the graphql-js validate mechanism. However, graphql-js validation rules don't have access to any request-specific context, and apollo-server doesn't provide a way for context-specific validation rules.

Perhaps we could give the validationDidStart plugin hook the ability to return/provide more validation rules? And also export NoIntrospection so it's easy to return from your own plugin hook? (Since it already returns an end hook, we either need to refactor it so that it can return a hook or an object with rules and a hook, or add a new callback, or something.)

@elier-lg In general it is a bad idea if your subgraph servers are directly accessible to end users, so locking down introspection there shouldn't be that relevant. That said, we generally recommend the use of managed federation rather than doing composition inside your graph for production work.

@uripre
Copy link

uripre commented Nov 29, 2021

@andyrichardson Could you clarify how this introduces a security vulnerability? How does the introspection "bypass" authentication?

@thekip
Copy link

thekip commented Jan 10, 2023

Using validation + plugin.

import { ApolloServerPlugin, GraphQLRequestListener } from 'apollo-server-plugin-base';
import { GraphQLRequestContext, GraphQLRequest } from 'apollo-server-types';
import { NoSchemaIntrospectionCustomRule, validate } from 'graphql';

const isDevelopment = process.env.NODE_ENV === 'development';

export class ConditionalIntrospectionGraphqlPlugin implements ApolloServerPlugin {
  constructor(private checkFn: (request: GraphQLRequest) => boolean) {
  }

  public async requestDidStart(_requestContext: GraphQLRequestContext<any>): Promise<GraphQLRequestListener> {
    return {
      didResolveOperation: async (ctx) => {
        if (isDevelopment) {
          return;
        }

        if (this.checkFn(_requestContext.request)) {
          return;
        }

        const errors = validate(ctx.schema, ctx.document, [NoSchemaIntrospectionCustomRule]);

        if (errors.length) {
          throw errors;
        }
      },
    };
  }
}

Then in apollo server config:

     // Introspection is controlled by ConditionalIntrospectionGraphqlPlugin
      introspection: true,
      plugins: [
        new ConditionalIntrospectionGraphqlPlugin((request) => {
          const key = request.http.headers.get(INTROSPECTION_HEADER);

          if (key) {
            return key === INTROSPECTION_KEY;
          }
        }),
      ],

Note, this NoSchemaIntrospectionCustomRule officially provided by graphql-js, and AFAIK used in the apollo server when introspection: false

@iUnstable0
Copy link

Using validation + plugin.

import { ApolloServerPlugin, GraphQLRequestListener } from 'apollo-server-plugin-base';
import { GraphQLRequestContext, GraphQLRequest } from 'apollo-server-types';
import { NoSchemaIntrospectionCustomRule, validate } from 'graphql';

const isDevelopment = process.env.NODE_ENV === 'development';

export class ConditionalIntrospectionGraphqlPlugin implements ApolloServerPlugin {
  constructor(private checkFn: (request: GraphQLRequest) => boolean) {
  }

  public async requestDidStart(_requestContext: GraphQLRequestContext<any>): Promise<GraphQLRequestListener> {
    return {
      didResolveOperation: async (ctx) => {
        if (isDevelopment) {
          return;
        }

        if (this.checkFn(_requestContext.request)) {
          return;
        }

        const errors = validate(ctx.schema, ctx.document, [NoSchemaIntrospectionCustomRule]);

        if (errors.length) {
          throw errors;
        }
      },
    };
  }
}

Then in apollo server config:

     // Introspection is controlled by ConditionalIntrospectionGraphqlPlugin
      introspection: true,
      plugins: [
        new ConditionalIntrospectionGraphqlPlugin((request) => {
          const key = request.http.headers.get(INTROSPECTION_HEADER);

          if (key) {
            return key === INTROSPECTION_KEY;
          }
        }),
      ],

Note, this NoSchemaIntrospectionCustomRule officially provided by graphql-js, and AFAIK used in the apollo server when introspection: false

dosent work for me

@AaronMoat
Copy link

If anyone wants a drop-in package for this, I've put something on npm: https://www.npmjs.com/package/apollo-server-plugin-conditional-introspection

@sb-onoffice-de
Copy link

sb-onoffice-de commented Feb 23, 2023

Using validation + plugin.

This works for us! Thank you!

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 19, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
⛲️ feature New addition or enhancement to existing solutions
Projects
None yet
Development

No branches or pull requests