Back when I used GraphiQL, it was just a simple express middleware I could wrap in whatever code I wanted, so I would get a username query parameter and generate a token for it to pass as the authorization header to GraphiQL.
But AFAIK from the Apollo Server docs and reading the code, I'm only able to provide static GraphQL playground config at construction time, so there's no way to do this.
Any real-world dev team isn't going to want to have to set the authorization header in GraphQL Playground by hand. I ended up copying out the code that calls renderPlaygroundPage so that when I navigate to /graphql?username=jimbob it automatically puts in the authorization header for jimbob. Had to put this into my own middleware that bypasses ApolloServer.applyMiddleware entirely.
I'm kind of angry about the hassles Apollo Server 2 has created. In the process of trying to make it easier for novice users to set up, you've made it harder to customize. The dumb use case is easier but for any real-world use case there's just one more layer of indirection (the ApolloServer class) to dig through the code for to figure out how to pass options through to the low-level stuff like express middleware and the subscription server.
Please bring back documentation for the piecemeal approach that relies on calling graphqlExpress etc. directly instead of creating an ApolloServer class.
I experienced the same issue. It prevents me to upgrade from graphql-server-express (deprecated) to apollo-server-express.
It seems the playground config is allowing to set headers for tabs. A simple solution would be to allow a function that take the request as input and returns the playground config as output. Something similar to what is possible with the context parameter:
new ApolloServer({
context: ({req, res}) => {
return {...};
},
introspection: true,
playground: ({req, res}) => {
return {...};
}
});
As a workaround, I have forked the Google cloud function adapter so that Playground default tab is initialized with the desired header. The modification consists in updating the following file:
Here are the quick and dirty lines added:
if (this.playgroundOptions.tabs && this.playgroundOptions.tabs[0]) {
this.playgroundOptions.tabs[0].headers = {Authorization: `Apikey ${req.query.accessToken}`};
}
I did something similar which didn't require forking or modifying Apollo internals for the moment.
In the context callback, you can inspect the request parameters and directly modify the request body (query, variables) or headers.
It's still not as good as being able to make the playground really dynamic, but takes the hassle out of doing a bunch of playground setup on every page-load (that configuration moves into the URL).
For the example above it'd be
new ApolloServer({
context: ({req, res}) => {
if (req.query.accessToken)
req.headers = {Authorization: `Apikey ${req.query.accessToken}`};
},
// ...
});
I have the same problem. I solved it by using the graphql playground directly as a middleware:
import expressPlayground from 'graphql-playground-middleware-express';
app.use(
'/graphql',
(req: express.Request, res: express.Response, next: express.NextFunction) => {
expressPlayground({
endpoint: `/graphql?headers=${encodeURIComponent(
JSON.stringify({
'x-csrf-token': `${req.cookies.csrf_token}`,
}),
)}`,
settings: {
...defaultPlaygroundOptions.settings,
'request.credentials': 'include',
},
})(req, res, next);
},
);
And disabled the playground in ApolloServer:
const apollo = new ApolloServer({
schema,
introspection: true,
playground: false,
...
});
It would be really nice to do this directly in ApolloServer, as lpellegr suggested.
@rgoldfinger I did pretty much the same thing but copied more of the code out of ApolloServer that sets up the playground middleware, because there's some non-trivial logic in there (which I don't want to have to keep copying whenever it's changed in ApolloServer). Also, FWIW, I didn't even have to disable the playground in ApolloServer because my custom middleware handles it first.
@rgoldfinger In order for what you have to work you have to import defaultPlaygroundOptions from apollo-server-express (or whatever integration you happen to be using). I assume you're doing that, but for the benefit of others:
import { defaultPlaygroundOptions } from 'apollo-server-express';
I'm also doing an additional spread to make sure I'm not missing anything:
app.get('/graphql', (req, res, next) => {
const headers = JSON.stringify({
'X-CSRF-Token': req.csrfToken(),
});
expressPlayground({
...defaultPlaygroundOptions,
endpoint: `/graphql?headers=${encodeURIComponent(headers)}`,
settings: {
...defaultPlaygroundOptions.settings,
'request.credentials': 'same-origin',
},
})(req, res, next);
});
I also had to use app.get for the playground middleware instead of app.use and explicitly place it before server.applyMiddleware({ app }) (I only use POST for queries) in order to get the playground and apollo to work together on the same endpoint.
With all of that out of the way, I'd like to point out how _simple_ this is with Graph_i_QL:
app.use('/graphiql', graphiqlExpress(req => ({
endpointURL: '/graphql',
passHeader: `'X-CSRF-Token': '${req.csrfToken()}',`,
})));
If we could restore that simplicity here that would be great. I just got done spending several hours getting basic security to work.
@rgoldfinger @knpwrs Thank you both guys! Your solution works seamlessly! This should be definitely fixed in apollo-server on its own.
GraphQL Playground should support the setting of tabs to any valid Tabs[] setting, which should include the ability to set options like endpoint, variables, headers, etc.:
From the GraphQL Playground documentation:
tabs Tab[] - An array of tabs.interface Tab {
endpoint: string
query: string
name?: string
variables?: string
responses?: string[]
headers?: { [key: string]: string }
}
And this should be possible to set within the Apollo Server constructor playground options. Please report back with a runnable reproduction (e.g. CodeSandbox, Glitch or a clone-able GitHub repository which can be npm started) if this is not the case on the latest version of Apollo Server, and we'll be happy to investigate further. Thanks!
@abernix - I think you are missing the point. We have no access to the request object in the playground settings, therefore, we cannot programmatically set the headers... Therefore, this isn't a solution.
I believe this one should be reopened, as it's quite frustrating.
There is no option to set the default headers for the playground, based on the currently logged-in user.
You're right, I did miss the point!
I can see how this helps in situations where headers need to be set dynamically based on, e.g. other headers, cookies, etc., but as we don't use the actual graphql-playground-express middleware, this sort of coupling would seem to be a bit difficult without an even closer coupling of GraphQL Playground and Apollo Server than we already have (which are separate projects!).
Is there any reason you wouldn't be able to disable the built-in Apollo Server GraphQL Playground integration (which merely renders the graphql-playground-html package which relies on the CDN-hosted graphql-playground-react package) and use the GraphQL Playground Express middleware directly (which uses graphql-playground-react)? This would allow you to leverage the Express middleware req object, and set headers accordingly.
I'd like to chime in as having this difficulty hitting me right in the face at the moment. I'm staring at having to spend several hours just to get basic security working in my GraphQL Playground, and I'm not looking forward to it.
@abernix the real problem is that since basic graphql playground is provided via ApolloServer, everyone starts out trying to work with that, so it takes hours to find out that they need to use the middleware and how to customize it to their needs. If you're not going to increase the coupling then the Apollo site at least needs to document, loudly, how to customize the middleware
Like, the documentation should start out by saying "you'll probably need to turn off the built-in playground and replace it with your own middleware so you can customize the headers etc." and then link to how to do that
@abernix maybe also move the slightly nontrivial logic in renderPlaygroundPage to its own package we can leverage in our customization, so that we don't have to fork that code and miss out on updates
+1, I ended up having to use the middleware in order to implement CSRF token (I'm using cookie-based auth because of SSR). It'd be really nice if it could be customized from the ApolloServer contructor settings.
I think I must be doing something wrong. Our authentication happens with the BearerStrategy handled by passport-azure-ad. So I copied a valid token from the front-end which is stored in the header not in a cookie. For one reason or another I still get unauthorized when trying to get to the http://localhost:5000/playground with the following code:
import { defaultPlaygroundOptions } from 'apollo-server-express'
import expressPlayground from 'graphql-playground-middleware-express'
const app = express()
app.use(express.urlencoded({ extended: true }))
app.use(cors({ origin: true }))
app.use(passport.initialize())
passport.use(bearerStrategy)
app.use(
passport.authenticate('oauth-bearer', { session: false }),
(req, _res, next) => {
console.log('User info: ', req.user)
console.log('Validated claims: ', req.authInfo)
next()
}
)
app.get('/playground', (req, res, next) => { // changed this to Playground as in the example on the website
const token = 'xxxMyTokenxxx'
const headers = JSON.stringify({
...req.headers,
authorization: `Bearer ${token}`
})
expressPlayground({
...defaultPlaygroundOptions,
endpoint: `/graphql?headers=${encodeURIComponent(headers)}`,
settings: {
...defaultPlaygroundOptions.settings,
'request.credentials': 'same-origin',
'tracing.tracingSupported': true, // I had to add this to avoid a TS error
},
})(req, res, next)
})
;(async () => {
try {
try {
await createConnections()
} catch (error) {
throw `Failed creating database connections: ${error}`
}
const server = await getApolloServer()
server.applyMiddleware({ app, cors: false })
app
.listen({ port: ENVIRONMENT.port }, () => {
console.log(
`Server ready at http://localhost:${ENVIRONMENT.port}${server.graphqlPath}`
)
})
.on('error', function (error) {
throw `Failed starting Express server on port ${ENVIRONMENT.port}: ${error}`
})
} catch (error) {
console.error(`Failed starting server: ${error}`)
process.exit(1)
}
})()
Am I doing something wrong here?
Oh, I think I've got it:
import expressPlayground from 'graphql-playground-middleware-express'
app.get('/playground', expressPlayground({ endpoint: '/graphql' }))
And then simply paste in the token in the Playground interface "HTTP HEADERS":
{
"authorization": "Bearer xxxMyTokenxxxx"
}
And now there are two different URL's each with its own purpose.
http://localhost:5000/playground
http://localhost:5000/graphql
It would also be advisable to disable the built-in Playground by apollo-server:
export const getApolloServer = async () => {
return new ApolloServer({
schema: await getSchema(),
context: ({ req }) => ({ getUser: () => req.user, }),
introspection: false,
playground: false,
})
}
One thing that doesn't work is reading the "Schema" or the "Docs" in the GUI. The "HTTP Headers" are probably not used for requesting these details and are as such blocked. They are presumably only used for firing the Graphql queries. This is a bit of a downside on this approach.
I hope this helps others struggling with this too. Thanks for the pointers guys, appreciate the help.
@abernix to put things another way, it was more straightforward setting up GraphiQL with our desired headers in Apollo 1; the way GraphQL Playground was built into Apollo 2 seems intended to be more convenient, but it actually ended up wasting a lot of time for anyone who didn't want to have to constantly copy and paste their auth header in.
The live Apollo Server docs still don't recommend just disabling the built-in GraphQL Playground and replacing it with your own middleware to set the auth headers...would you accept a PR to the docs that adds a section about that?
@rgoldfinger I just noticed you're sending a headers query parameter:
/graphql?headers=${encodeURIComponent(
That's actually pretty convenient in that it allows any tab opened in the playground to use those headers (and maybe preserves history?) but I assume you had to make your own middleware to handle that query parameter?
Most helpful comment
@rgoldfinger In order for what you have to work you have to import
defaultPlaygroundOptionsfromapollo-server-express(or whatever integration you happen to be using). I assume you're doing that, but for the benefit of others:I'm also doing an additional spread to make sure I'm not missing anything:
I also had to use
app.getfor the playground middleware instead ofapp.useand explicitly place it beforeserver.applyMiddleware({ app })(I only usePOSTfor queries) in order to get the playground and apollo to work together on the same endpoint.With all of that out of the way, I'd like to point out how _simple_ this is with Graph_i_QL:
If we could restore that simplicity here that would be great. I just got done spending several hours getting basic security to work.