zabumba
zabumba

Reputation: 12422

GraphQL Gateway `preflight request doesn't pass access control check`

I have a GraphQL gateway to federate a couple of subgraphs. The gateway executes incoming operations across those subgraphs.

Observed ERROR in the browser CONSOLE

OPTIONS method is triggered (preflight), nothing in the backend logs. The error observed in the browser below

enter image description here

Access to fetch at 'http://dev.gateway.mydomain.net:5000/graphql' 
from origin 'http://localhost:3000' 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. 
If an opaque response serves your needs, set the request's mode to 'no-cors' 
to fetch the resource with CORS disabled.

Network

Access-Control-Allow-Origin: *

enter image description here

OPTIONS METHODS

enter image description here

GraphQL Gateway server

Below code for my Apollo gateway

I have marked as comments everything I have tried (TRY 1..7) to resolve my CORS issue

const { ApolloServer } = require('apollo-server-express');
const { ApolloGateway, RemoteGraphQLDataSource } = require('@apollo/gateway');
const express = require('express');
const path = require('path');
const expressJwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');
const expressSession = require('express-session');
const passport = require('passport');
const Auth0Strategy = require('passport-auth0');
const jsonwebtoken = require('jsonwebtoken');
const dotenv = require('dotenv');
const cors = require('cors');
const userInViews = require('./lib/middleware/userInViews');
const authRouter = require('./routes/auth');
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
const graphqlRouter = require('./routes/graphql');
const headersRouter = require('./routes/headers');

dotenv.config({ path: `./gateway.${process.env.NODE_ENV}.env` });

const port = 5000;

const strategy = new Auth0Strategy(
  {
    domain: process.env.AUTH0_DOMAIN,
    clientID: process.env.AUTH0_CLIENT_ID,
    clientSecret: process.env.AUTH0_CLIENT_SECRET,
    callbackURL: process.env.AUTH0_CALLBACK_URL || `http://localhost:${port}/callback`,
  },
  (accessToken, refreshToken, extraParams, profile, done) => {
    console.log('ACCESS TOKEN', accessToken);
    const decoded = jsonwebtoken.decode(accessToken);
    const customProfile = profile;
    customProfile.permissions = decoded.permissions;
    customProfile.authorization = `Bearer ${accessToken}`;
    return done(null, customProfile);
  },
);

passport.use(strategy);

passport.serializeUser((user, done) => {
  done(null, user);
});

passport.deserializeUser((user, done) => {
  done(null, user);
});

const session = {
  secret: 'FILTERED',
  cookie: {},
  resave: false,
  saveUninitialized: true,
};

const app = express();
app.use(express.static('assets'));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

if (app.get('env') === 'production') {
  session.cookie.secure = true;
  app.set('trust proxy', 1);
}

app.use(expressSession(session));
app.use(passport.initialize());
app.use(passport.session());
app.use(userInViews()); // Application level middleware: Add user to view
app.use('/', authRouter);
app.use('/', indexRouter);
app.use('/', usersRouter);
app.use('/', graphqlRouter);
app.use('/', headersRouter);
app.set('json spaces', 2);

const localUserLoginMutationCheck = expressJwt({
  secret: 'f.....Y',
  algorithms: ['HS256'],
  credentialsRequired: false,
});

const auth0AuthenticationCheck = expressJwt({
  secret: jwksRsa.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`,
  }),
  audience: 'http://dev.gateway.mydomain.net:5000',
  issuer: [`https://${process.env.AUTH0_DOMAIN}/`],
  algorithms: ['RS256'],
  credentialsRequired: false,
});

app.use(
  auth0AuthenticationCheck,
);

/**
* TRY 1
*/
const corsOptions = {
  origin: '*',
  credentials: true, // access-control-allow-credentials:true
  optionSuccessStatus: 200,
};
app.use(cors(corsOptions));

/**
* TRY 2
*/
app.use((req, res, next) => {
  console.log('INCOMING REQUEST HEADERS', req.headers);
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
  console.log('INCOMING REQUEST HEADERS AFTER', req.headers);
  next();
});

const gateway = new ApolloGateway({
  serviceList: [
    { name: 'siebel', url: 'http://localhost:4000/graphql' },
    { name: 'sciencelogic', url: 'http://localhost:4001/graphql' },
  ],
  buildService({ name, url }) {
    return new RemoteGraphQLDataSource({
      url,
      willSendRequest({ request, context }) {
        console.log('CONTEXT', context);
        request.http.headers.set(
          'user',
          context.user
            ? JSON.stringify(context.user)
            : null,
        );
      },
    });
  },
});


const gqlServer = new ApolloServer({
  gateway,
  /**
  * TRY 3
  */
  // cors: true,

  /**
  * TRY 4
  */
  // cors: {
  //   origin: '*',
  //   methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
  //   preflightContinue: false,
  //   optionsSuccessStatus: 204,
  //   credentials: true,
  // },

  /**
  * TRY 5
  */
  // cors: {
  //   credentials: true,
  //   origin: (origin, callback) => {
  //     const whitelist = [
  //       'http://localhost:3000',
  //       'https://stage-tool.mydomain.net',
  //       'https://stage.tool.mydomain.net',
  //       'https://dsr.mydomain.net',
  //     ];
  //     if (whitelist.indexOf(origin) !== -1) {
  //       callback(null, true);
  //     } else {
  //       callback(new Error('Not allowed by CORS'));
  //     }
  //   },
  // },

  subscriptions: false,
  context: ({ req }) => {
    const user = req.user || null;
    return { user };
  },
  introspection: true,
});


// const corsOptions = {
//   origin: 'http://localhost:3000',
//   credentials: true,
// };
gqlServer.applyMiddleware({
  app,
  /**
  * TRY 6
  */
  cors: false, 

  /**
  * TRY 7
  */
  // cors: corsOptions,
});

app.listen({ port }, () => {
  console.log(`Gateway ready at http://localhost:${port}${gqlServer.graphqlPath}`);
});

Subgraph server

const stackTrace = require('stack-trace');
const { ApolloServer } = require('apollo-server');
const { applyMiddleware } = require('graphql-middleware');
const { buildFederatedSchema } = require('@apollo/federation');
const jwt = require('jsonwebtoken');
const nodemailer = require('nodemailer');
const GqlHelpers = require('../gql-helpers');
const { siebelPermissions } = require('../../permissions');

const SiebelContactIntegration = require('./lib/ads-contact');

console.debug(`env ${JSON.stringify(process.env, null, 2)}`);
const resolvers = require(`./lib/gql/siebel.resolvers.${process.env.NODE_ENV}`);
const siebelSchema = GqlHelpers.gqlImport(__dirname, `./lib/gql/siebel.${process.env.NODE_ENV}.graphql`);
const typeDefs = GqlHelpers.gqlWrapper([siebelSchema]);

const federatedSchema = buildFederatedSchema([{ typeDefs, resolvers }]);

const server = new ApolloServer(
  {
    cors: true,
    schema: applyMiddleware(
      federatedSchema,
      siebelPermissions,
    ),
    context: ({ req }) => {
      const jwtToken = req.headers.authorization ? req.headers.authorization : null;
      console.log('HEADERS (SIEBEL)', req.headers);
      const jwtTokenArray = jwtToken ? jwtToken.split(' ') : [];
      const decodedJwt = jwtTokenArray.length > 0 ? jwt.decode(jwtTokenArray[1]) : null;
      const jwtIssuer = decodedJwt && decodedJwt.iss ? decodedJwt.iss : null;
      const jwtSubject = decodedJwt && decodedJwt.sub ? decodedJwt.sub : null;

      const user = req.headers.user ? JSON.parse(req.headers.user) : null;
      return { user, jwtIssuer, jwtSubject };
    },
    dataSources: () => ({
      siebelContactIntegration: new SiebelContactIntegration(),
    }),
    cacheControl: {
      defaultMaxAge: 3600,
    },
    introspection: true,
  },
);

server.listen(
  {
    port: 4000,
  },
  () => {
    console.log(`🚀 Siebel GraphQL Server ready at ${process.env.SAPI_URL}${server.graphqlPath}`);
  },
);

ReactJs Client using apollo-client library

Note the fetch options set to no-cors

import React, { useEffect, useState } from "react";
import {
  ApolloClient,
  InMemoryCache,
  HttpLink,
  ApolloProvider,
} from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { useAuth0 } from "@auth0/auth0-react";

function ApolloWrapper({ children }) {
  const { isAuthenticated, getAccessTokenSilently } = useAuth0();
  const [bearerToken, setBearerToken] = useState("");
  const httpLink = new HttpLink({
    uri: "http://dev.gateway.mydomain.net:5000/graphql",
  });

  useEffect(() => {
    const getToken = async () => {
      const token = isAuthenticated ? await getAccessTokenSilently() : "";
      setBearerToken(token);
      console.log(token);
    };
    getToken();
  }, [getAccessTokenSilently, isAuthenticated]);

  const authLink = setContext((_, { headers, ...rest }) => {
    if (!bearerToken) return { headers, ...rest };
    return {
      ...rest,
      headers: {
        ...headers,
        authorization: `Bearer ${bearerToken}` || null,
      },
    };
  });
  const client = new ApolloClient({
    cache: new InMemoryCache(),
    link: authLink.concat(httpLink),
    fetchOptions: {
      mode: 'no-cors',   // <== Note the fetch options
    },
  });

  return <ApolloProvider client={client}>{children}</ApolloProvider>;
}

export default ApolloWrapper;

using Apollo client useLazyQuery to execute the GraphQL query.

import { useLazyQuery } from "@apollo/client";

const [getEmail, { loading, error, data }] = useLazyQuery(
  gqlSiebelFindContact,
  {
    errorPolicy: "all",
  }
);

Any pointers?

I am trying to figure out why I am getting the preflight request doesn't pass access control check and whether the issue is to be fixed on the frontend or backend code.

I am able to workaround the problem by configuring my Chrome browser for no CORS.

Any further pointers to troubleshoot and find the ground issue are most welcome

LINKS

Upvotes: 4

Views: 4500

Answers (1)

Heiko Thei&#223;en
Heiko Thei&#223;en

Reputation: 17487

The error message points to a failed preflight request, which is an OPTIONS request without credentials. Such preflight requests should be handled by the app.use(cors(corsOptions)) middleware. But this middleware comes rather late, so this will not work if any of the earlier middlewares (e.g., app.use(passport.session())) already responds to this OPTIONS request.

The screenshot of the OPTIONS request with Content-Length: 8 in the response seems to imply that this happens here.

Upvotes: 1

Related Questions