Apollo client QUERIES not sending headers to server but mutations are fine - graphql

I hooked up a front end to a graphql server. Most if not all the mutations are protected while all the queries are not protected. I have an auth system in place where if you log in, you get an access/refresh token which all mutations are required to use. And they do which is great, backend receives the headers and everything!
HOWEVER. There is one query that needs at least the access token to distinguish the current user! BUT the backend does not receive the two headers! I thought that the middlewareLink I created would be for all queries/mutations but I'm wrong and couldn't find any additional resources to help me out.
So here's my setup
apollo-client.js
import { InMemoryCache } from "apollo-cache-inmemory"
import { persistCache } from "apollo-cache-persist"
import { ApolloLink } from "apollo-link"
import { HttpLink } from "apollo-link-http"
import { onError } from "apollo-link-error"
import { setContext } from "apollo-link-context"
if (process.browser) {
try {
persistCache({
cache,
storage: window.localStorage
})
} catch (error) {
console.error("Error restoring Apollo cache", error)
}
}
const httpLink = new HttpLink({
uri: process.env.GRAPHQL_URL || "http://localhost:4000/graphql"
})
const authMiddlewareLink = setContext(() => ({
headers: {
authorization: localStorage.getItem("apollo-token") || null,
"x-refresh-token": localStorage.getItem("refresh-token") || null
}
}))
const afterwareLink = new ApolloLink((operation, forward) =>
forward(operation).map(response => {
const context = operation.getContext()
const {
response: { headers }
} = context
if (headers) {
const token = headers.get("authorization")
const refreshToken = headers.get("x-refresh-token")
if (token) {
localStorage.setItem("apollo-token", token)
}
if (refreshToken) {
localStorage.setItem("refresh-token", refreshToken)
}
}
return response
})
)
const errorLink = onError(({ graphQLErrors, networkError }) => {
...
// really long error link code
...
})
let links = [errorLink, afterwareLink, httpLink]
if (process.browser) {
links = [errorLink, afterwareLink, authMiddlewareLink, httpLink]
}
const link = ApolloLink.from(links)
export default function() {
return {
cache,
defaultHttpLink: false,
link
}
}
Is there a way to target ALL mutations/queries with custom headers not just mutations? Or apply some headers to an individual query since I could probably put that as an app middleware?
edit: Haven't solved the SSR portion of this yet.. will re-edit with the answer once I have.

Related

Is there a way to get a hash out of an Apollo request

After trying for hours to try implementing a Hmac token on a react Apollo client request, I just couldn't find a way to retrieve the final request body that is being sent.
Is there a way to get the request body sent to the server before the client sends it or no way at all ? Not being able to get body hash causes for some security concerns for Mitm attacks since using a jwt the way Apollo docs show it doesn't provide with a way of signing the content.
Based on this, I came up with something
import React from "react";
import { render } from "react-dom";
import {
ApolloClient,
ApolloProvider,
HttpLink,
ApolloLink,
InMemoryCache,
concat,
useQuery,
gql,
} from "#apollo/client";
import CryptoJS from "crypto-js";
import { print } from "graphql/language/printer";
const httpLink = new HttpLink({ uri: "http://localhost:3000/dev/query" });
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
const { query, variables, operationName } = operation,
payload = {
variables,
operationName,
query: print(query),
};
console.log(payload);
// custom headers
const token = getAuthHeader(payload, "key"), // create HMAC here
headers = {
hmac: token,
};
operation.setContext({
headers: {
...headers,
},
});
return forward(operation);
});
const client = new ApolloClient({
cache: new InMemoryCache({
addTypename: false, //quite important
}),
link: concat(authMiddleware, httpLink),
});
function getAuthHeader(payload_string, sk) {
var str = JSON.stringify(payload_string);
console.log(str);
var hmac = CryptoJS.HmacSHA256(str, sk).toString(CryptoJS.enc.Hex);
return hmac;
}
function MakeRequest() {
const request = `
query {
items {
_id
}
}
`;
const { loading, error, data } = useQuery(gql(request));
//Whatever you wanna do with it.
}
function App() {
return (
<div>
<MakeRequest />
</div>
);
}
render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById("root")
);
Seems to work correctly.

Apollo Server - Apply Authentication to Certain Resolvers Only with Passport-JWT

I currently have a Node.js back-end running Express with Passport.js for authentication and am attempting to switch to GraphQL with Apollo Server. My goal is to implement the same authentication I am using currently, but cannot figure out how to leave certain resolvers public while enabling authorization for others. (I have tried researching this question extensively yet have not been able to find a suitable solution thus far.)
Here is my code as it currently stands:
My JWT Strategy:
const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = JWT_SECRET;
module.exports = passport => {
passport.use(
new JwtStrategy(opts, async (payload, done) => {
try {
const user = await UserModel.findById(payload.sub);
if (!user) {
return done(null, false, { message: "User does not exist!" });
}
done(null, user);
} catch (error) {
done(err, false);
}
})
);
}
My server.js and Apollo configuration:
(I am currently extracting the bearer token from the HTTP headers and passing it along to my resolvers using the context object):
const apollo = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
let authToken = "";
try {
if (req.headers.authorization) {
authToken = req.headers.authorization.split(" ")[1];
}
} catch (e) {
console.error("Could not fetch user info", e);
}
return {
authToken
};
}
});
apollo.applyMiddleware({ app });
And finally, my resolvers:
exports.resolvers = {
Query: {
hello() {
return "Hello world!";
},
async getUserInfo(root, args, context) {
try {
const { id } = args;
let user = await UserModel.findById(id);
return user;
} catch (error) {
return "null";
}
},
async events() {
try {
const eventsList = await EventModel.find({});
return eventsList;
} catch (e) {
return [];
}
}
}
};
My goal is to leave certain queries such as the first one ("hello") public while restricting the others to requests with valid bearer tokens only. However, I am not sure how to implement this authorization in the resolvers using Passport.js and Passport-JWT specifically (it is generally done by adding middleware to certain endpoints, however since I would only have one endpoint (/graphql) in this example, that option would restrict all queries to authenticated users only which is not what I am looking for. I have to perform the authorization in the resolvers somehow, yet not sure how to do this with the tools available in Passport.js.)
Any advice is greatly appreciated!
I would create a schema directive to authorized query on field definition and then use that directive wherever I want to apply authorization. Sample code :
class authDirective extends SchemaDirectiveVisitor {
visitObject(type) {
this.ensureFieldsWrapped(type);
type._requiredAuthRole = this.args.requires;
}
visitFieldDefinition(field, details) {
this.ensureFieldsWrapped(details.objectType);
field._requiredAuthRole = this.args.requires;
}
ensureFieldsWrapped(objectType) {
// Mark the GraphQLObjectType object to avoid re-wrapping:
if (objectType._authFieldsWrapped) return;
objectType._authFieldsWrapped = true;
const fields = objectType.getFields();
Object.keys(fields).forEach(fieldName => {
const field = fields[fieldName];
const {
resolve = defaultFieldResolver
} = field;
field.resolve = async function (...args) {
// your authorization code
return resolve.apply(this, args);
};
});
}
}
And declare this in type definition
directive #authorization(requires: String) on OBJECT | FIELD_DEFINITION
map schema directive in your schema
....
resolvers,
schemaDirectives: {
authorization: authDirective
}
Then use it on your api end point or any object
Query: {
hello { ... }
getuserInfo():Result #authorization(requires:authToken) {...}
events():EventResult #authorization(requires:authToken) {...}
};

apolloClient.query not using middleware, while <Query /> does

I have an apolloclient with middleware that console.logs a bearer token, because I am not always authenticated when I should be.
For some reason, it appears that queries from the react-apollo <Query /> object use this middleware -- I see my console message -- but queries that I trigger programmatically with: apolloClient.query do not log anything (there's no way for the code to do this, the console log is at the top of the authLink middleware).
I started my project with apollo-boost before switching to apolloclient, so I thought perhaps node_modules was not correctly set up after the switch. But I've removed and reinstalled with yarn, it should not have any vestiges of apollo-boost in there now.
additionally, if I copy the code that I use to create apolloclient into my transaction, making it use that local copy instead of the global one, the middleware DOES fire.
ie:
export const relayBBNToGraphcool = async () => {
/* BEGIN without this code, WHICH IS ALREADY in the instantiation of apolloClient, the result is `user: null` */
const authLink = setContext(async (req, { headers }) => {
// get the authentication token from local storage if it exists
let getToken = async () => await AsyncStorage.getItem(/*'access_token'*/'graphcool_token')
const token = await getToken()
console.trace('token for connection to graphcool is currently', token, req.operationName)
// return the headers to the context so httpLink can read them
return token
? {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : null,
}
}
: { headers }
})
const httpLink = new HttpLink(config)
const link = ApolloLink.from([/* retryLink, */ authLink, httpLink])
const cache = new InMemoryCache()
// overriding apolloClient in the global scope of this module
const apolloClient = new ApolloClient({
link,
cache
})
/* END */
apolloClient.query({ query: User.self, forceFetch: true })
.then(authneticatedUser => {
console.trace('response', authneticatedUser)
if(authneticatedUser.data.user === null)
throw ('no user')
apolloClient is configured from apollo-client not apollo-boost. It is attached to its provider in App.js:
return (
<ApolloProvider client={this.state.apolloClient}>
that is loaded from a different file with getApolloClient() -- which sets a local variable apolloClient:
var apolloClient //...
export const getApolloClient = () => { // ...
apolloClient = new ApolloClient({
link,
cache
}) //...
return apolloClient
all calls to .query or .mutate are done from exported functions in this same file, and they use that same var apolloClient. I do not ever instantiate more than one apollo-client. Why is it that some of my queries are firing the middleware, but others are not ?
edit:
per request, the actual links used:
// from src: https://github.com/kadikraman/offline-first-mobile-example/blob/master/app/src/config/getApolloClient.js
export const getApolloClient = async () => {
const retryLink = new RetryLink({
delay: {
initial: 1000
},
attempts: {
max: 1000,
retryIf: (error, _operation) => {
if (error.message === 'Network request failed') {
//if (_operation.operationName === 'createPost')
// return true
}
return false
}
}
})
// from: https://www.apollographql.com/docs/react/recipes/authentication.html
const authLink = setContext(async (req, { headers }) => {
// get the authentication token from local storage if it exists
let getToken = async () => await AsyncStorage.getItem(/*'access_token'*/'graphcool_token')
const token = await getToken()
console.trace('token for connection to graphcool is currently', token, req.operationName)
// return the headers to the context so httpLink can read them
return token
? {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : null,
}
}
: { headers }
})
const httpLink = new HttpLink(config)
const link = ApolloLink.from([retryLink, authLink, httpLink])
const cache = new InMemoryCache()
apolloClient = new ApolloClient({
link,
cache
})
try {
await persistCache({
cache,
storage: AsyncStorage
})
} catch (err) {
console.error('Error restoring Apollo cache', err) // eslint-disable-line no-console
}
return apolloClient
}
It turns out that the problem has something to do with cache -- this section in the getApolloClient method:
try {
await persistCache({
cache,
storage: AsyncStorage
})
} catch (err) {
console.error('Error restoring Apollo cache', err) // eslint-disable-line no-console
}
It works if I change the code to save apolloClient before that change is applied to the copy sent to ApolloProvider, like this:
export var apolloClient
// from src: https://github.com/kadikraman/offline-first-mobile-example/blob/master/app/src/config/getApolloClient.js
export const getApolloClient = async () => {
apolloClient = await getRawClient()
try {
await persistCache({
cache,
storage: AsyncStorage
})
} catch (err) {
console.error('Error restoring Apollo cache', err) // eslint-disable-line no-console
}
return apolloClient
}
export const getRawClient = async () => {
const retryLink = new RetryLink({
delay: {
initial: 1000
},
attempts: {
max: 1000,
retryIf: (error, _operation) => {
if (error.message === 'Network request failed') {
//if (_operation.operationName === 'createPost')
// return true
}
return false
}
}
})
// from: https://www.apollographql.com/docs/react/recipes/authentication.html
const authLink = setContext(async (req, { headers }) => {
// get the authentication token from local storage if it exists
let getToken = async () => await AsyncStorage.getItem(/*'access_token'*/'graphcool_token')
const token = await getToken()
console.trace('token for connection to graphcool is currently', token, req.operationName)
// return the headers to the context so httpLink can read them
return token
? {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : null,
}
}
: { headers }
})
const httpLink = new HttpLink(config)
const link = ApolloLink.from([/* retryLink, */ authLink, httpLink])
const cache = new InMemoryCache()
return new ApolloClient({
link,
cache
})
}
Then, I also refactor the query & mutate code out of this file, importing apolloClient. That works... which is kinda bizarre, but whatever.

Accessing Mutation Result in Angular Apollo Graphql

I am new to Graphql and I am using the Apollo client with Angular 7.
I have a mutation in the server that I am using for authentication.This mutation generates returns an access token and a refresh token:
#Injectable({
providedIn: "root"
})
export class LoginTokenAuthGQL extends Apollo.Mutation<
LoginTokenAuth.Mutation,
LoginTokenAuth.Variables
> {
document: any = gql`
mutation loginTokenAuth($input: LoginAuthInput!) {
loginTokenAuth(input: $input) {
accessToken
refreshToken
}
}
`;
}
I am running this mutation in my sign-in component like this:
onLoginSubmit() {
const email = this.loginForm.controls['userEmail'].value;
const password = this.loginForm.controls['userPassword'].value;
console.log('Sending mutation with', email, password);
this.loginGQL.mutate({
input: {
email,
password,
userType: AuthUserType.Crm
}
}).pipe(
map((response) => response.data )
).subscribe(
(output: LoginTokenAuth.Mutation) => {
console.log('Access token', output.loginTokenAuth.accessToken);
console.log('Refresh token', output.loginTokenAuth.refreshToken);
console.log(this.apollo.getClient().cache);
},
((error: any) => {
console.error(error);
})
);
}
Once I get the access token I will need to add it as header on my requests.
From what I read from the Apollo Client all results from queries and mutations are cached locally in the client. But it is not clear to me how can I access them and add it to the apollo-link.
To be more clear I would like to do this in my Graphql module:
const http = httpLink.create({uri: '/graphql'});
const auth = setContext((_, { headers }) => {
// get the authentication token from the cache
const token = ???
if (!token) {
return {};
} else {
return {
headers: headers.append('Authorization', `Bearer ${token}`)
};
}
});
Even official docs of Apollo Client suggest you store this token as usually - to localStorage.
import { ApolloClient } from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import { InMemoryCache } from 'apollo-cache-inmemory';
const httpLink = createHttpLink({
uri: '/graphql',
});
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = localStorage.getItem('token');
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
}
}
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
});

Stitching secure subscriptions using makeRemoteExecutableSchema

We have implemented schema stitching where GraphQL server fetches schema from two remote servers and stitches them together. Everything was working fine when we were only working with Query and Mutations, but now we have a use-case where we even need to stitch Subscriptions and remote schema has auth implemented over it.
We are having a hard time figuring out on how to pass authorization token received in connectionParams from client to remote server via the gateway.
This is how we are introspecting schema:
API Gateway code:
const getLink = async(): Promise<ApolloLink> => {
const http = new HttpLink({uri: process.env.GRAPHQL_ENDPOINT, fetch:fetch})
const link = setContext((request, previousContext) => {
if (previousContext
&& previousContext.graphqlContext
&& previousContext.graphqlContext.request
&& previousContext.graphqlContext.request.headers
&& previousContext.graphqlContext.request.headers.authorization) {
const authorization = previousContext.graphqlContext.request.headers.authorization;
return {
headers: {
authorization
}
}
}
else {
return {};
}
}).concat(http);
const wsLink: any = new WebSocketLink(new SubscriptionClient(process.env.REMOTE_GRAPHQL_WS_ENDPOINT, {
reconnect: true,
// There is no way to update connectionParams dynamically without resetting connection
// connectionParams: () => {
// return { Authorization: wsAuthorization }
// }
}, ws));
// Following does not work
const wsLinkContext = setContext((request, previousContext) => {
let authToken = previousContext.graphqlContext.connection && previousContext.graphqlContext.connection.context ? previousContext.graphqlContext.connection.context.Authorization : null
return {
context: {
Authorization: authToken
}
}
}).concat(<any>wsLink);
const url = split(({query}) => {
const {kind, operation} = <any>getMainDefinition(<any>query);
return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLinkContext,
link)
return url;
}
const getSchema = async (): Promise < GraphQLSchema > => {
const link = await getLink();
return makeRemoteExecutableSchema({
schema: await introspectSchema(link),
link,
});
}
const linkSchema = `
extend type UserPayload {
user: User
}
`;
const schema: any = mergeSchemas({
schemas: [linkSchema, getSchema],
});
const server = new GraphQLServer({
schema: schema,
context: req => ({
...req,
})
});
Is there any way for achieving this using graphql-tools? Any help appreciated.
I have one working solution: the idea is to not create one instance of SubscriptionClient for the whole application. Instead, I'm creating the clients for each connection to the proxy server:
server.start({
port: 4000,
subscriptions: {
onConnect: (connectionParams, websocket, context) => {
return {
subscriptionClients: {
messageService: new SubscriptionClient(process.env.MESSAGE_SERVICE_SUBSCRIPTION_URL, {
connectionParams,
reconnect: true,
}, ws)
}
};
},
onDisconnect: async (websocket, context) => {
const params = await context.initPromise;
const { subscriptionClients } = params;
for (const key in subscriptionClients) {
subscriptionClients[key].close();
}
}
}
}, (options) => console.log('Server is running on http://localhost:4000'))
if you would have more remote schemas you would just create more instances of SubscriptionClient in the subscriptionClients map.
To use those clients in the remote schema you need to do two things:
expose them in the context:
const server = new GraphQLServer({
schema,
context: ({ connection }) => {
if (connection && connection.context) {
return connection.context;
}
}
});
use custom link implementation instead of WsLink
(operation, forward) => {
const context = operation.getContext();
const { graphqlContext: { subscriptionClients } } = context;
return subscriptionClients && subscriptionClients[clientName] && subscriptionClients[clientName].request(operation);
};
In this way, the whole connection params will be passed to the remote server.
The whole example can be found here: https://gist.github.com/josephktcheung/cd1b65b321736a520ae9d822ae5a951b
Disclaimer:
The code is not mine, as #josephktcheung outrun me with providing an example. I just helped with it a little. Here is the original discussion: https://github.com/apollographql/graphql-tools/issues/864
This is a working example of remote schema with subscription by webscoket and query and mutation by http. It can be secured by custom headers(params) and shown in this example.
Flow
Client request
-> context is created by reading req or connection(jwt is decoded and create user object in the context)
-> remote schema is executed
-> link is called
-> link is splitted by operation(wsLink for subscription, httpLink for queries and mutations)
-> wsLink or httpLink access to context created above (=graphqlContext)
-> wsLink or httpLink use context to created headers(authorization header with signed jwt in this example) for remote schema.
-> "subscription" or "query or mutation" are forwarded to remote server.
Note
Currently, ContextLink does not have any effect on WebsocketLink. So, instead of concat, we should create raw ApolloLink.
When creating context, checkout connection, not only req. The former will be available if the request is websocket, and it contains meta information user sends, like an auth token.
HttpLink expects global fetch with standard spec. Thus, do not use node-fetch, whose spec is incompatible (especially with typescript). Instead, use cross-fetch.
const wsLink = new ApolloLink(operation => {
// This is your context!
const context = operation.getContext().graphqlContext
// Create a new websocket link per request
return new WebSocketLink({
uri: "<YOUR_URI>",
options: {
reconnect: true,
connectionParams: { // give custom params to your websocket backend (e.g. to handle auth)
headers: {
authorization: jwt.sign(context.user, process.env.SUPER_SECRET),
foo: 'bar'
}
},
},
webSocketImpl: ws,
}).request(operation)
// Instead of using `forward()` of Apollo link, we directly use websocketLink's request method
})
const httpLink = setContext((_graphqlRequest, { graphqlContext }) => {
return {
headers: {
authorization: jwt.sign(graphqlContext.user, process.env.SUPER_SECRET),
},
}
}).concat(new HttpLink({
uri,
fetch,
}))
const link = split(
operation => {
const definition = getMainDefinition(operation.query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink, // <-- Executed if above function returns true
httpLink, // <-- Executed if above function returns false
)
const schema = await introspectSchema(link)
const executableSchema = makeRemoteExecutableSchema({
schema,
link,
})
const server = new ApolloServer({
schema: mergeSchemas([ executableSchema, /* ...anotherschemas */]),
context: ({ req, connection }) => {
let authorization;
if (req) { // when query or mutation is requested by http
authorization = req.headers.authorization
} else if (connection) { // when subscription is requested by websocket
authorization = connection.context.authorization
}
const token = authorization.replace('Bearer ', '')
return {
user: getUserFromToken(token),
}
},
})

Resources