Reputation: 9684
I'm pretty much copy/pasting the example code from the Apollo GraphQL Network Layer Docs on Creating Middleware, except I'm pulling my dependencies from apollo-client-preset
and I'm using a simple utility library to return the jwt token. Here's the link code...
import { ApolloClient, HttpLink, InMemoryCache, ApolloLink, concat } from 'apollo-client-preset';
import { getSessionToken } from './api/localStorage';
const httpLink = new HttpLink({ uri: 'http://localhost:4000/' });
const authMiddleware = new ApolloLink((operation, forward) => {
const token = getSessionToken();
const authorizationHeader = token ? `Bearer ${token}` : null;
operation.setContext({
headers: {
authorization: authorizationHeader,
}
});
return forward(operation);
})
const client = new ApolloClient({
link: concat(authMiddleware, httpLink),
});
And here's the ./api/localStorage
method in play...
export const getSessionToken = () => {
return AsyncStorage.getItem(AUTH_TOKEN);
}
Executing the above code results in an HTTP request with the following headers...
POST / HTTP/1.1
Host: localhost:4000
Content-Type: application/json
User-Agent: Expo/2.2.0.1011489 CFNetwork/893.14 Darwin/17.3.0
Connection: keep-alive
Accept: */*
Accept-Language: en-us
Authorization: Bearer [object Object]
Accept-Encoding: gzip, deflate
Content-Length: 174
Notice the
Authorization: Bearer [object Object]
bit
I think the problem is pretty obvious. AsyncStorage is an async function, and I need to wait for the promise to be resolved before I assemble the authorization string. The solution, however, is less obvious. I've tried various methods, but no love so far.
I've seen a couple examples similar to the above. Here's another in the How to GraphQL Authentication Module. Neither shows how to wait for the asyncstorage promise to resolve first. I know Apollo waits for the promises for you automagically sometimes, so I thought that might be the case here. But doesn't seem to be in my experience.
So what's the solution?
const middlewareAuthLink = new ApolloLink((operation, forward) => {
getSessionToken().then(token => {
const authorizationHeader = token ? `Bearer ${token}` : null
operation.setContext({
headers: {
authorization: authorizationHeader
}
})
});
return forward(operation)
})
This does construct the authorization string properly, but doesn't seem to create the middleware because this does not create the authorization header. To me, this seems like my best bet, but I must be doing it wrong for reasons which are not obvious to me.
POST / HTTP/1.1
Host: localhost:4000
Content-Type: application/json
Connection: keep-alive
Accept: */*
User-Agent: Expo/2.2.0.1011489 CFNetwork/893.14 Darwin/17.3.0
Content-Length: 174
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Notice the lack of
Authorization
header
I thought this would be pretty straight forward, turning this into an async/await function, but not so much. Here's the code
const middlewareAuthLink = new ApolloLink(async (operation, forward) => {
const token = await getSessionToken();
const authorizationHeader = token ? `Bearer ${token}` : null;
operation.setContext({
headers: {
authorization: authorizationHeader,
}
});
return forward(operation)
})
But that resulted in this super ugly red-screen-of-death error...
Unhandled (in react-apollo:Apollo(EventList)), ApolloError@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:106405:36
currentResult@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:106516:43
dataForChild@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:101608:79
render@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:101659:49
finishClassComponent@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:4528:102
performUnitOfWork@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:5547:33
workLoop@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:5566:142
_invokeGuardedCallback@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:2707:23
invokeGuardedCallback@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:2681:41
performWork@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:5602:41
scheduleUpdateImpl@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:5723:105
enqueueForceUpdate@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:4341:179
forceUpdate@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:8265:38
forceRenderChildren@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:101579:58
next@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:101554:50
error@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:109797:25
forEach@[native code]
error@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:106747:44
http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:107277:47
http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:107649:29
forEach@[native code]
http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:107648:27
forEach@[native code]
broadcastQueries@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:107644:33
http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:107239:51
tryCallOne@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:10901:14
http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:10987:25
_callTimer@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:12157:15
_callImmediatesPass@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:12193:17
callImmediates@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:12397:31
__callImmediates@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:2301:30
http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:2179:32
__guard@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:2287:11
flushedQueue@http://localhost:19001/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&strict=false&minify=false&hot=false&assetPlugin=/Users/ChrisGeirman/dev/mobile/playground/exp/frogquest-app/node_modules/expo/tools/hashAssetFiles:2178:19
flushedQueue@[native code]
invokeCallbackAndReturnFlushedQueue@[native code]
Upvotes: 3
Views: 1635
Reputation: 10922
Here is an alternative which loads the token from an in memory cache which should perform better than fetching from AsyncStorage
. Alternatively you could simply have a getter wrapper function which caches the response from AsyncStorage
but having this as part of apollo cache has the added benefit of re-rendering when you update the token.
import gql from 'graphql-tag'
import ApolloClient from 'apollo-client'
import { setContext } from 'apollo-link-context'
import { createHttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { ApolloLink } from 'apollo-link'
import AsyncStorage from '@react-native-community/async-storage'
const authLink = setContext(async (_, { headers }) => {
const context = await client.query({
query: gql`
query AuthQuery {
authToken @client
}
`
})
const {
data: { authToken: token }
} = context
let extra = {}
if (token) {
extra = { authorization: `Bearer ${token}` }
}
return {
headers: {
...headers,
...extra
}
}
})
const httpLink = createHttpLink({
uri: 'http://localhost:4000/'
})
const cache = new InMemoryCache()
const client = new ApolloClient({
link: ApolloLink.from([authLink, httpLink]),
cache
})
client.writeData({ data: { authToken: null } })
AsyncStorage.getItem('token').then((token: string | null) => {
if (token) {
client.writeData({ data: { authToken: token } })
}
})
export { client }
Upvotes: 0
Reputation: 9684
There may be other solutions, or better solutions even, but this finally worked for me so I'm posting it as solved.
import { setContext } from 'apollo-link-context';
const httpLink = new HttpLink({ uri: 'http://localhost:4000/'});
const authHeader = setContext(
request =>
new Promise((success, fail) => {
getSessionToken().then(token => success({ headers: { authorization: `Bearer ${token}` }}))
})
)
const client = new ApolloClient({
link: concat(authHeader, httpLink),
cache: new InMemoryCache(),
})
Upvotes: 7