Forward request headers - apollo federated gateway - node.js

I'm using the following versions:
"#apollo/gateway": "^2.1.3"
"#apollo/server": "^4.0.0"
"graphql": "^16.6.0"
I can't get a handle on the req object to extract the headers and forward them. The buildService code works to add headers to requests to downstream services, but the context on ApolloServer is consistently empty. I tried sync and async, request instead of req. I even tried grabbing them directly from context.req.headers, but that's null.
Anyone have any idea on how to accomplish this?
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: "persons", url: process.env.PERSON_SERVER_URL },
],
}),
buildService({ url }) {
return new RemoteGraphQLDataSource({
url,
willSendRequest: ({ request, context }) => {
console.log(JSON.stringify(context));
// TRYING TO INJECT AUTH HEADERS HERE
}
});
}
});
const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer({
gateway,
context: ({ req }) => {
console.log(JSON.stringify(req));
// req IS NULL
},
plugins: [
ApolloServerPluginLandingPageDisabled(),
ApolloServerPluginDrainHttpServer({ httpServer })
]
});
await server.start();
const graphqlRoute = "/graphql";
app.use(
graphqlRoute,
bodyParser.json(),
expressMiddleware(server),
);
await new Promise((resolve) => httpServer.listen(process.env.PORT, "0.0.0.0", resolve));
console.log(`πŸš€ Server ready at ${JSON.stringify(httpServer.address())}`);
For what it's worth, I asked here, as well. This feels like it should be a simple flag (especially for federation) to forward the Authorization header.

you need to read headers from request in expressMiddleware, next save them in context and then they will be available in willSendRequest, try:
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: "persons", url: process.env.PERSON_SERVER_URL },
],
}),
buildService({ url }) {
return new RemoteGraphQLDataSource({
url,
willSendRequest: ({ request, context }) => {
console.log(JSON.stringify(context));
for (const [headerKey, headerValue] of Object.entries(context.headers)) {
request.http?.headers.set(headerKey, headerValue);
}
}
});
}
});
const app = express();
const httpServer = http.createServer(app);
const server = new ApolloServer({
gateway,
plugins: [
ApolloServerPluginLandingPageDisabled(),
ApolloServerPluginDrainHttpServer({ httpServer })
]
});
await server.start();
const graphqlRoute = "/graphql";
async function context({ req }) {
return {
headers: req.headers,
};
}
app.use(
graphqlRoute,
bodyParser.json(),
expressMiddleware(server, {context: context}),
);
await new Promise((resolve) => httpServer.listen(process.env.PORT, "0.0.0.0", resolve));
console.log(`πŸš€ Server ready at ${JSON.stringify(httpServer.address())}`);

in AS4, ApolloServer has an API that isn't specific to a framework, and the argument to the context function is framework-specific. a la
app.use(
graphqlRoute,
cors(),
bodyParser.json(),
expressMiddleware(server, {
context: async ({ req }) => ({
token: req.headers.authorization
}),
}),
);

Related

Clickjacking Issue In Shopify Nextjs App?

I am facing an issue regarding clickjacking prevention. I have implemented authentication and custom headers that contain headers that are requested by Shopify.
ctx.set('Content-Security-Policy', `frame-ancestors https://${ctx.query.shop} https://admin.shopify.com`);
ctx.res.setHeader(
"Content-Security-Policy",
`frame-ancestors https://${ctx.query.shop} https://admin.shopify.com;`
)
local environment headers:
[1]: https://i.stack.imgur.com/XBkW4.png
Production Environment response headers:
[2]: https://i.stack.imgur.com/YkfnA.png
It is working fine in my development environment but is not working in production.
my server.js file
/* eslint-disable #typescript-eslint/no-var-requires */
const dotenv = require('dotenv')
const Koa = require('koa')
const next = require('next')
const { default: createShopifyAuth } = require('#shopify/koa-shopify-auth')
const { verifyRequest } = require('#shopify/koa-shopify-auth')
const { default: Shopify, ApiVersion } = require('#shopify/shopify-api')
const { withSentry } = require('#sentry/nextjs')
const proxy = require('koa-proxy')
const Router = require('#koa/router')
const { checkForNewThemeSupport } = require('./checkfordawntheme')
dotenv.config()
const { PORT } = process.env
const { NODE_ENV } = process.env
const port = parseInt(PORT, 10) || 3000
const dev = NODE_ENV !== 'production'
const app = next({ dev })
const handle = withSentry(app.getRequestHandler())
if (!process.env.NEXT_PUBLIC_SHOPIFY_API_KEY || !process.env.SHOPIFY_API_SECRET_KEY) {
console.error('Missing api keys')
}
const SCOPES = [
'read_products',
'write_products',
'unauthenticated_read_product_listings',
'read_orders',
'read_script_tags',
'write_script_tags',
'read_themes'
]
Shopify.Context.initialize({
API_KEY: process.env.NEXT_PUBLIC_SHOPIFY_API_KEY,
API_SECRET_KEY: process.env.SHOPIFY_API_SECRET_KEY,
SCOPES,
HOST_NAME: process.env.SHOPIFY_APP_URL.replace(/https:\/\//, ''),
API_VERSION: ApiVersion.October20,
IS_EMBEDDED_APP: true,
SESSION_STORAGE: new Shopify.Session.MemorySessionStorage()
})
// TODO replace this with something serious
const ACTIVE_SHOPIFY_SHOPS = {}
const handleRequest = async (ctx) => {
await handle(ctx.req, ctx.res)
ctx.set('Content-Security-Policy', `frame-ancestors https://${ctx.query.shop} https://admin.shopify.com`);
ctx.res.setHeader(
"Content-Security-Policy",
`frame-ancestors https://${ctx.query.shop} https://admin.shopify.com;`
)
console.log("************* frame ancestor ********* ")
ctx.respond = false
ctx.res.statusCode = 200
}
app.prepare().then(() => {
const server = new Koa()
const router = new Router()
server.keys = [Shopify.Context.API_SECRET_KEY]
// online auth for app/user request
server.use(createShopifyAuth({
accessMode: 'online',
afterAuth(ctx) {
// Online access mode access token and shop available in ctx.state.shopify
const { shop } = ctx.state.shopify
const { host } = ctx.query
// Redirect to app with shop parameter upon auth
ctx.redirect(`/?shop=${shop}&host=${host}`)
}
}))
// offline auth for background tasks
server.use(createShopifyAuth({
accessMode: 'offline',
prefix: '/offline',
async afterAuth(ctx) {
const { shop, accessToken } = ctx.state.shopify
ACTIVE_SHOPIFY_SHOPS[shop] = true
// APP_UNINSTALLED webhook to make sure merchants go through OAuth if they reinstall it
const response = await Shopify.Webhooks.Registry.register({
shop,
accessToken,
path: '/webhooks',
topic: 'APP_UNINSTALLED',
webhookHandler: async (topic, shop) => delete ACTIVE_SHOPIFY_SHOPS[shop]
})
if (!response.success) {
console.error(`Failed to register APP_UNINSTALLED webhook: ${response.result}`)
}
ctx.redirect(`/auth?shop=${shop}`)
}
}))
router.get('/', async (ctx) => {
const { shop } = ctx.query
if (ACTIVE_SHOPIFY_SHOPS[shop] === undefined) {
ctx.redirect(`/offline/auth?shop=${shop}`)
} else {
await handleRequest(ctx)
}
})
router.get('(/_next/static/.*)', handleRequest)
router.get('/_next/webpack-hmr', handleRequest)
router.get('/(.*).js', handleRequest)
router.get('/login', verifyRequest(), handleRequest)
router.get('/register', verifyRequest(), handleRequest)
router.post(
'/webhooks',
async (ctx) => {
try {
await Shopify.Webhooks.Registry.process(ctx.req, ctx.res)
} catch (error) {
console.error(`Failed to process webhook: ${error}`)
}
}
)
router.post(
'/graphql',
verifyRequest({ returnHeader: true }),
async (ctx) => {
await Shopify.Utils.graphqlProxy(ctx.req, ctx.res)
}
)
router.get(
'/checkfor20',
verifyRequest(),
async (ctx) => {
try {
const hasCartAppBlock = await checkForNewThemeSupport(ctx)
ctx.body = JSON.stringify({ hasCartAppBlock })
ctx.status = 200
} catch (error) {
console.error(`Failed to check for theme: ${error}`)
}
}
)
server.use(router.allowedMethods())
server.use(router.routes())
server.use(proxy({
host: process.env.NEXT_PUBLIC_TREEPOINTS_API_URL,
match: /^\/********-api\/\w*/,
map: (path) => path?.split(process.env.NEXT_PUBLIC_TREEPOINTS_API_PROXY_URL)?.[1] || path
}))
// eslint-disable-next-line no-console
server.listen(port, () => console.log(`> Ready on http://localhost:${port}`))
})
App development framework: nextjs
deployment server: Heroku
Any help would be appreciated.

Uploading Files on production server returns either CORS error or POST 400 Bad Request using Apollo-Graphql

I'm having trouble on uploading files to my production server, in a local environment, everything works fine just as expected, but when I try to do a a Mutation containing file uploads (and only on those mutations) it throws me a CORS error. I'm using Spaces to upload the files and save them into my Database.
app.ts (Back-end):
const configureExpress = async () => {
const app: express.Application = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use('/services', servicesRoute);
const { typeDefs, resolvers } = await buildSchema;
const schema = makeExecutableSchema({ typeDefs, resolvers });
const server = new ApolloServer({
schema,
playground: true,
introspection: true,
uploads: {
maxFileSize: 1000000000,
maxFiles: 100,
},
context: async ({ req }) => ({
auth: await Auth.getUser(req),
}),
formatError: (err) => ({
message: err.message,
code: err.extensions && err.extensions.code,
locations: err.locations,
path: err.path,
extensions: err.extensions && err.extensions.exception,
}),
});
server.applyMiddleware({ app });
return app;
};
export default () => database.connect().then(configureExpress);
client-auth.ts (On the Front-end):
const errorLink = onError(({ graphQLErrors }: any) => {
if (graphQLErrors) {
console.log(graphQLErrors);
graphQLErrors.map(({ message }: any) => console.log(message));
graphQLErrors.map(({ code }: any) => {
if (code === 'UNAUTHENTICATED') {
persistStore(store)
.purge()
.then(() => {
window.location.reload();
});
}
return true;
});
}
});
const authLink = setContext((_, { headers }) => {
const token = store.getState().auth.user!.token;
return {
headers: {
...headers,
authorization: `Bearer ${token}`,
},
};
});
const uploadLink = createUploadLink({
uri: 'https://api.example.com.br/graphql'
// uri: 'http://localhost:4000/graphql',
});
const client = new ApolloClient({
link: ApolloLink.from([errorLink, authLink, uploadLink]),
cache: new InMemoryCache(),
defaultOptions: {
query: {
fetchPolicy: 'network-only',
},
},
});
resolver.ts
return propertyModel.create({
...data
} as DocumentType<any>).then(async property => {
const user = await userModel.findById(input.userId);
if (!user) throw new UserNotFound();
await ownerModel.findByIdAndUpdate(user.owner, {
$push: {
properties: property.id,
}
});
if (input.images) {
input.images.forEach(async image => {
const uploadedImage = await FileS3.upload(image, {
path: 'images',
id: propertyId.toHexString(),
});
await property.updateOne({$push: {images: uploadedImage}});
});
}
if (input.scripture) {
const uploadedScripture = await FileS3.upload(input.scripture, {
path: 'documents',
id: propertyId.toHexString(),
});
await property.updateOne({scripture: uploadedScripture});
}
if (input.registration) {
const uploadedRegistration = await FileS3.upload(input.registration, {
path: 'documents',
id: propertyId.toHexString(),
})
await property.updateOne({
registration: uploadedRegistration,
});
};
if (input.car) {
const uploadedCar = await FileS3.upload(input.car, {
path: 'documents',
id: propertyId.toHexString(),
});
await property.updateOne({
car: uploadedCar,
});
};
if (input.ccir) {
const uploadedCcir = await FileS3.upload(input.ccir, {
path: 'documents',
id: propertyId.toHexString(),
});
await property.updateOne({
ccir: uploadedCcir,
});
};
if (input.itr) {
const uploadedItr = await FileS3.upload(input.itr, {
path: 'documents',
id: propertyId.toHexString(),
});
await property.updateOne({
itr: uploadedItr,
});
};
if (input.subscription) {
const uploadedSubscription = await FileS3.upload(input.subscription, {
path: 'documents',
id: propertyId.toHexString(),
});
await property.updateOne({
subscription: uploadedSubscription
});
return property;
});
};
I'm really lost regarding this error, is this an actual server error? (Production server is on DigitalOcean in Ubuntu) or something wrong regarding the code?
For CORS, if you are using the latest version of ApolloServer then turn on the CORS:
const server = new ApolloServer({
cors: {
credentials: true,
origin: true
},,
...
});
//also apply it here
server.applyMiddleware({ app,
cors: {
credentials: true,
origin: true
}
});
400 status code is returned for bad request which happens when a client sends a malformed request to server, You need to verify that your client is sending the correct data and headers on via correct HTTP verb (post/get etc)
If any one happens to have this same problem, here's how I solved.
After digging through the code I realized that in the server I was receiving a Maximum call stack size exceeded, upon looking further to this problem I realized that It was an error regarding the Graphql-Upload dependency, I fixed it by removing it as a dependency and added the following on my package.json:
"resolutions": {
"fs-capacitor":"^6.2.0",
"graphql-upload": "^11.0.0"
}
after this I just executed this script: npx npm-force-resolutions. And It worked all fine.

nuxtjs apollo-client does not set authorization header

I am trying to create a login functionality using nuxtjs with the nuxtjs apollo-module and nodejs in the backend using apollo-server. I would like to pass the token from the frontend (nuxtjs/apollo-client) to the backend (nodejs/apollo-server).
Signin Function (frontend)
async signin () {
const email = this.email
const password = this.password
try {
const res = await this.$apollo.mutate({
mutation: signIn,
variables: {
email,
password
}
}).then(({ data }) => data && data.signIn)
const token = res.token
await this.$apolloHelpers.onLogin(token)
this.$router.push('/feed')
} catch (err) {
// Error message
}
}
nuxtjs.config (frontend)
apollo: {
clientConfigs: {
default: {
httpEndpoint: 'http://localhost:8000/graphql',
wsEndpoint: 'ws://localhost:8000/graphql',
authenticationType: 'Bearer',
httpLinkOptions: {
credentials: 'include'
},
}
}
Cookie in Browser DevTools
Index File (backend)
const app = express()
const corsConfig = {
origin: 'http://127.0.0.1:3000',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
credentials: true
}
app.use(cors(corsConfig))
app.use(morgan('dev'))
const getMe = async req => {
const token = req.headers.authorization // <========
console.log(token) // returns 'Bearer undefined'
if (token) {
try {
return await jwt.verify(token, process.env.SECRET)
} catch (e) {
// Error message
}
}
}
const server = new ApolloServer({
introspection: true,
playground: true,
typeDefs: schema,
resolvers,
context: async ({ req }) => {
if (req) {
const me = await getMe(req)
return {
models,
me,
secret: process.env.SECRET,
loaders: {
user: new DataLoader(keys =>
loaders.user.batchUsers(keys, models),
),
},
}
}
},
})
server.applyMiddleware({
app,
path: '/graphql',
cors: false
})
const httpServer = http.createServer(app)
server.installSubscriptionHandlers(httpServer)
const port = process.env.PORT || 8000
sequelize.sync({ force: true }).then(async () => {
createUsers(new Date())
httpServer.listen({ port }, () => {
console.log(`Apollo Server on http://localhost:${port}/graphql`)
})
})
The token is saved in a cookie called 'apollo-token'. However the Authoriation header in the format 'Bearer token' is not set. According to the apollo-client documentation this should be set automatically (https://github.com/nuxt-community/apollo-module#authenticationtype-string-optional-default-bearer).
What am I missing? I would be very thankful for any kind of help!

Fetch API failed to Fetch during authentication, alongside CORS error

I have a button that lauches a fetch to my API that uses KOA and JWT. The javascript for the fetch initiated on click is:
<script>
function loginButton(user, pass) {
fetch('http://localhost:5454/api/login', {
method: "post",
headers: {
'Content-Type': "application/json"
},
body: JSON.stringify({
username: user,
password: pass
})
})
.then( (response) => {
console.log("Success")
})
.catch(e => console.log(e));
}
</script>
The code for my Authentication is:
router.post(`${BASE_URL}/login`, async (ctx) => {
const reqUsername = ctx.request.body.username
const reqPassword = ctx.request.body.password
const unauthorized = (ctx) => {
ctx.status = 401
ctx.body = {
error: 'Invalid username or password'
}
}
let attemptingUser
try {
attemptingUser = await Employee.findOne({ where: { username: reqUsername }})
if (attemptingUser != null && attemptingUser.password === reqPassword) {
ctx.status = 200
ctx.body = {
username: attemptingUser.username,
given_name: attemptingUser.given_name,
role: attemptingUser.role,
created_at: attemptingUser.createdAt,
updated_at: attemptingUser.updatedAt,
}
const token = jwt.sign({ username: attemptingUser.username, role: attemptingUser.role }, SECRET)
ctx.set("X-Auth", token)
} else {
unauthorized(ctx)
}
} catch(err) {
console.error(err)
console.error(`Failed to find username: ${reqUsername}`)
unauthorized(ctx)
}
})
The code for my KOA initiation is:
require('dotenv').config()
const Koa = require('koa')
const Router = require('koa-router')
const bodyParser = require('koa-bodyparser')
const baseRoutes = require('./routes')
const cors = require('#koa/cors');
const PORT = process.env.PORT || 8080
const app = new Koa()
app.use(bodyParser())
app.use(baseRoutes.routes())
app.use(cors());
app.listen(PORT, () => {
console.log(`Server listening on ${PORT}`)
})
Im using Port 8080 for my http-server and port 5454 for my npm server. I am getting a Failed to Fetch in the catch of the Fetch, as well as a CORS error related to not having a Access-Control-Allow-Origin header in the response header. I've tried a couple things and am ready to have a new set of eyes look at it, any tips?
Edit: I am successfully receiving the token in the X-Auth header, but for some reason it’s still throwing errors and I’d like to get them resolved before it spirals out of control.

Test a secure graphql subscription

I am trying to test a configuration for securing graphql subscriptions in my application.
This is my config in the ApolloServer constructor:
const app = express();
const jwt_authentication = jwt({
secret: JWT_SECRET,
credentialsRequired: false
})
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: true,
playground: true,
formatError: error => {
console.log(error);
},
context: async ({req, connection }) => {
if (connection) {
return connection.context;
} else {
return some_method_to_return_user_info;
}
},
subscriptions: {
onConnect: async (connectionParams, webSocket, context) => {
const user = await jsonwebtoken.verify(connectionParams.jwt, JWT_SECRET);
const userInfo= some_method_to_return_user_info;
if (userInfo) {
return { user: userInfo };
}
throw new Error("Unauthorized subscription");
}
}
});
app.use(GRAPHQL_PATH, jwt_authentication);
//...
When I run a subscription in GraphQL Playground I get the error:
jwt must be provided
I tested with the header "Authorization": "Bearer MY_TOKEN" and then with "jwt": "MY_TOKEN", but I believe that it's not as straightforward as that.
Is there any possibility to test my subscriptions without implementing a client code?
I got it working in GraphQL Playground by adding the HTTP Header that way:
{
"jwt": "MY_TOKEN"
}

Resources