(2 minute read)
A requirement of our Ethereum Dapp is that we sign the user into our backend prior to allowing them to make an modification to either on-chain (smart contract) or off-chain (cloud db) data.
Since we're using GraphQL for our frontend to backend API connection we decided to introduce a directive - @auth - for use with our GraphQL queries and mutations that would indicate when the user needed to first be logged in prior to executing the call.
For example, to deploy a new on-chain contact instance we first need to log the user in (duh!), so the GraphQL query would be:
mutation create($name: String, $deposit: String, $limitOfParticipants: String) {
create(name: $name, deposit: $deposit, limitOfParticipants: $limitOfParticipants) @auth
}
Doing an authenticated request requires adding the Authorization header to the outgoing AJAX call. The easiest way to add such a header when using the Apollo GraphQL client is through writing a custom Apollo link. A Link allows one to modify a GraphQL request and its corresponding response whilst in transit. By default you may use a HTTP Link to send GraphQL requests to a server, or a Local Link to serve requests locally within the client itself.
Our custom link will look for the @auth directive in a request, strip it out of the request, and add an Authorization header to the request before the request reaches the HTTP Link that will actually send it to our backend server:
import gql from 'graphql-tag'
import { Observable, ApolloLink } from 'apollo-link'
import { hasDirectives, checkDocument, removeDirectivesFromDocument } from 'apollo-utilities'
const sanitizedQueryCache = new Map()
const authManager = {
isLoggedIn: () => { /* return if user logged in */ },
logIn: () => { /* show modal/page/etc to log user in */ },
authHeaderValue: () => { /* return "Bearer 0x23..." */ },
}
export default new ApolloLink((operation, forward) => {
// if no @auth directive then nothing further to do!
if (!hasDirectives(['auth'], operation.query)) {
return forward(operation)
}
// get sanitized query (remove @auth directive since server won't understand it)
const cacheKey = JSON.stringify(operation.query)
let sanitizedQuery = sanitizedQueryCache[cacheKey]
if (!sanitizedQuery) {
// remove directives (inspired by https://github.com/apollographql/apollo-link-state/blob/master/packages/apollo-link-state/src/utils.ts)
checkDocument(operation.query)
sanitizedQuery = removeDirectivesFromDocument( [{ name: 'auth' }], operation.query)
// save to cache for next time!
sanitizedQueryCache[cacheKey] = sanitizedQuery
}
// overwrite original query with sanitized version
operation.query = sanitizedQuery
// build handler
return new Observable(async observer => {
let handle
// if user is not logged in
if (!authManager.isLoggedIn()) {
try {
await authManager.logIn()
} catch (err) {
console.error(err)
observer.complete([])
}
}
// add auth headers (by this point we should have them!)
operation.setContext({
headers: {
Authorization: authManager.authHeaderValue()
}
})
// pass request down the chain
handle = forward(operation).subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
})
// return unsubscribe function
return () => {
if (handle) {
handle.unsubscribe()
}
}
})
})
The key things to note from above:
We would make use of this Link as follows when setting up ApolloClient:
import { ApolloLink } from 'apollo-link'
import { HttpLink } from 'apollo-link-http'
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import auth from './auth' // our custom Link
const http => new HttpLink({ uri: `https://mybackend.com/graphql` })
const cache = new InMemoryCache()
const clientInstance = new ApolloClient({
cache,
link: ApolloLink.from([
auth, /* comes before http */
http
])
})
The only part of the code I haven't written out fully is the authManager. This is specific to each app - in our case it dispays a sign-in modal when the use wishes to sign in, and it saves the authentication token obtained into Local Storage so that on page reloads the user is still signed in.
You can see our full working code example at https://github.com/noblocknoparty/app/blob/dev/src/graphql/links/auth.js.