Test a secure graphql subscription - node.js

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"
}

Related

Forward request headers - apollo federated gateway

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
}),
}),
);

Context is empty in GraphQL middleware

I'm sending from frontend authorization token in headers and then I want to check validity of this token in some endpoints using middleware and context, but context is always empty.
I'm using type-graphql.
Frontend code (I check request in 'Network' tab and I can see my additional header):
private async mutate<T>(
mutation: DocumentNode,
data: unknown,
token?: string
) {
const response = await apolloClient.mutate<T>({
mutation: mutation,
context: {
headers: {
'auth-token': token || '',
},
},
variables: {
data: data,
},
});
return response.data;
}
Resolver code:
#Mutation(() => Token)
#UseMiddleware(authMiddleware)
async login(#Ctx() ctx: unknown, #Arg('data') data: LoginInput) {
console.log(ctx);
...
}
Middleware code:
export const authMiddleware: MiddlewareFn = ({ context }, next) => {
console.log(context);
try {
return next();
} catch (error) {
return next();
}
};
console.log is always equal to {}
I found the cause.
In declaration of ApollorServer the context was missing.
const server = new ApolloServer({
schema,
context: ({ req }) => {
const context = {
req,
};
return context;
},
cors: {
origin: '*',
credentials: true,
},
});

Request logging in Apollo GraphQL

Environment: Node app based on Apollo GraphQL server (direct Apollo server, no express middleware)
I need to intercept requests and log log them at certain points of the processing pipelines. Here is what I have so far:
const server = new ApolloServer({
// code removed for clarity
context: async ({ req }) => {
// here is the first request log, preparing the context for the upcoming calls (traceability)
},
formatError: async (err: any) => {
// Here I would like to finish logging, but no context is available
},
Problems are traceability of different logs from the same end user request and logging of the successful requests.
1- How can I relate the request context within the formatError method?
2- Where should I implement the logging of the successfully executed requests?
Aleks,
You can create custom error classes which can be extended from ApolloError or node Error classes. By assigning context to the error object from the resolver function, you can use it from formatError method.
formatResponse method can be used here which provides requestContext as as argument.
formatError and formatResponse methods usage:
const { ApolloServer, gql, makeExecutableSchema } = require("apollo-server");
const typeDefs = gql`
type User {
name: String
username: String
statusCode: Int
}
type Query {
user: User
}
`;
const dummyUser = { username: "dummyUser", name: "Dummy user" };
const user = () => {
// throw new Error('Test error');
return dummyUser;
};
const resolvers = {
Query: { user }
};
const schema = makeExecutableSchema({
typeDefs,
resolvers,
});
const server = new ApolloServer({
context: ({ req }) => ({ headers: req.headers }),
schema,
introspection: true,
playground: true,
formatResponse: (response) => {
response.data.user.statusCode = 200;
return response;
},
formatError: (error) => {
error.message = "Error name";
return error;
}
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
Trying out in middleware might be an alternate approach for both(not sure about this).

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!

Resources