CORS
CORS stands for Cross Origin Resource Sharing. In a nutshell, by default, browsers aren't allowed to access resources outside their own domain.
When you need to worry about CORS
If your api and web sides are deployed to different domains, you'll have to worry about CORS. For example, if your web side is deployed to example.com
but your api is api.example.com
. For security reasons your browser will not allow XHR requests (like the kind that the GraphQL client makes) to a domain other than the one currently in the browser's address bar.
This will become obvious when you point your browser to your site and see none of your GraphQL data. When you look in the web inspector you'll see a message along the lines of:
⛔️ Access to fetch https://api.example.com has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Avoiding CORS
Dealing with CORS can complicate your app and make it harder to deploy to new hosts, run in different environments, etc. Is there a way to avoid CORS altogether?
Yes! If you can add a proxy between your web and api sides, all requests will appear to be going to and from the same domain (the web side, even though behind the scenes they are forwarded somewhere else). This functionality is included automatically with hosts like Netlify or Vercel. With a host like Render you can enable a proxy with a simple config option. Most providers should provide this functionality through a combination of provider-specific config and/or web server configuration.
GraphQL Config
You'll need to add CORS headers to GraphQL responses. You can do this easily enough by adding the cors
option in api/src/functions/graphql.js
(or graphql.ts
):
export const handler = createGraphQLHandler({
loggerConfig: { logger, options: {} },
directives,
sdls,
services,
+ cors: {
+ origin: 'https://www.example.com', // <-- web side domain
+ },
onException: () => {
db.$disconnect()
},
})
Note that the origin
needs to be a complete URL including the scheme (https
). This is the domain that requests are allowed to come from. In this example we assume the web side is served from https://www.example.com
. If you have multiple servers that should be allowed to access the api, you can pass an array of them instead:
cors: {
origin: ['https://example.com', 'https://www.example.com']
},
The proper one will be included in the CORS header depending on where the response came from.
Authentication Config
The following config only applies if you're using dbAuth, which is Redwood's own cookie-based auth system.
You'll need to configure several things:
- Add CORS config for GraphQL
- Add CORS config for the auth function
- Cookie config for the auth function
- Allow sending of credentials in GraphQL XHR requests
- Allow sending of credentials in auth function requests
Here's how you configure each of these:
GraphQL CORS Config
You'll need to add CORS headers to GraphQL responses, and let the browser know to send up cookies with any requests. Add the cors
option in api/src/functions/graphql.js
(or graphql.ts
) with an additional credentials
property:
export const handler = createGraphQLHandler({
loggerConfig: { logger, options: {} },
directives,
sdls,
services,
+ cors: {
+ origin: 'https://www.example.com', // <-- web side domain
+ credentials: true,
+ },
onException: () => {
db.$disconnect()
},
})
origin
is the domain(s) that requests come from (the web side).
Auth CORS Config
Similar to the cors
options being sent to GraphQL, you can set similar options in api/src/functions/auth.js
(or auth.ts
):
const authHandler = new DbAuthHandler(event, context, {
db: db,
authModelAccessor: 'user',
authFields: {
id: 'id',
username: 'email',
hashedPassword: 'hashedPassword',
salt: 'salt',
resetToken: 'resetToken',
resetTokenExpiresAt: 'resetTokenExpiresAt',
},
+ cors: {
+ origin: 'https://www.example.com', // <-- web side domain
+ credentials: true,
+ },
cookie: {
HttpOnly: true,
Path: '/',
SameSite: 'Strict',
Secure: true,
},
forgotPassword: forgotPasswordOptions,
login: loginOptions,
resetPassword: resetPasswordOptions,
signup: signupOptions,
})
Just like the GraphQL config, origin
is the domain(s) that requests come from (the web side).
Cookie Config
In order to be able accept cookies from another domain we'll need to make a change to the SameSite
option in api/src/functions/auth.js
and set it to None
:
cookie: {
HttpOnly: true,
Path: '/',
SameSite: 'None',
Secure: true,
},
GraphQL XHR Credentials
Next we need to tell the GraphQL client to include credentials (the dbAuth cookie) in any requests. This config goes in web/src/App.js
:
const App = () => (
<FatalErrorBoundary page={FatalErrorPage}>
<RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
<AuthProvider type="dbAuth">
<RedwoodApolloProvider
graphQLClientConfig={{
httpLinkConfig: { credentials: 'include' },
}}
>
<Routes />
</RedwoodApolloProvider>
</AuthProvider>
</RedwoodProvider>
</FatalErrorBoundary>
)
Auth XHR Credentials
Finally, we need to tell dbAuth to include credentials in its own XHR requests:
const App = () => (
<FatalErrorBoundary page={FatalErrorPage}>
<RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
<AuthProvider
type="dbAuth"
config={{ fetchConfig: { credentials: 'include' } }}
>
<RedwoodApolloProvider
graphQLClientConfig={{
httpLinkConfig: { credentials: 'include' },
}}
>
<Routes />
</RedwoodApolloProvider>
</AuthProvider>
</RedwoodProvider>
</FatalErrorBoundary>
)
Testing CORS Locally
If you've made the configuration changes above, localhost
testing should continue working as normal. But, if you want to make sure your CORS config works without deploying to the internet somewhere, you'll need to do some extra work.
Serving Sides to the Internet
First, you need to get the web and api sides to be serving from different hosts. A tool like ngrok or localhost.run allows you to serve your local development environment over a real domain to the rest of the internet (on both http
and https
).
You'll need to start two tunnels, one for the web side (this example assumes ngrok):
> ngrok http 8910
Session Status online
Account Your Name (Plan: Pro)
Version 2.3.40
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://3c9913de0c00.ngrok.io -> http://localhost:8910
Forwarding https://3c9913de0c00.ngrok.io -> http://localhost:8910
And another for the api side:
> ngrok http 8911
Session Status online
Account Your Name (Plan: Pro)
Version 2.3.40
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://fb6d701c44b5.ngrok.io -> http://localhost:8911
Forwarding https://fb6d701c44b5.ngrok.io -> http://localhost:8911
Note the two different domains. Copy the https
domain from the api side because we'll need it in a moment. Even if the Redwood dev server isn't running you can leave these tunnels running, and when the dev server does start, they'll just start on those domains again.
redwood.toml
Config
You'll need to make two changes here:
- Bind the server to all network interfaces
- Point the web side to the api's domain
Normally the dev server only binds to 127.0.0.1
(home sweet home) which means you can only access it from your local machine using localhost
or 127.0.0.1
. To tell it to bind to all network interfaces, and to be available to the outside world, add this host
option:
[web]
title = "Redwood App"
port = 8910
host = '0.0.0.0'
apiUrl = '/.redwood/functions'
includeEnvironmentVariables = []
[api]
port = 8911
[browser]
open = true
We'll also need to tell the web side where the api side lives. Update the apiUrl
to whatever domain your api side is running on (remember the domain you copied from from ngrok):
[web]
title = "Redwood App"
port = 8910
host = '0.0.0.0'
apiUrl = 'https://fb6d701c44b5.ngrok.io'
includeEnvironmentVariables = []
[api]
port = 8911
[browser]
open = true
Where you get this domain from will depend on how you expose your app to the outside world (this example assumes ngrok).
Starting the Dev Server
You'll need to apply an option when starting the dev server to tell it to accept requests from any host, not just localhost
:
> yarn rw dev --fwd="--allowed-hosts all"
Wrapping Up
Now you should be able to open the web side's domain in a browser and use your site as usual. Test that GraphQL requests work, as well as authentication if you are using dbAuth.