1---
2title: Authentication
3order: 6
4---
5
6# Authentication
7
8Most APIs include some type of authentication, usually in the form of an auth token that is sent with each request header.
9
10The purpose of the [`authExchange`](../api/auth-exchange.md) is to provide a flexible API that facilitates the typical
11JWT-based authentication flow.
12
13> **Note:** [You can find a code example for `@urql/exchange-auth` in an example in the `urql` repository.](https://github.com/urql-graphql/urql/tree/main/examples/with-refresh-auth)
14
15## Typical Authentication Flow
16
17**Initial login** — the user opens the application and authenticates for the first time. They enter their credentials and receive an auth token.
18The token is saved to storage that is persisted though sessions, e.g. `localStorage` on the web or `AsyncStorage` in React Native. The token is
19added to each subsequent request in an auth header.
20
21**Resume** — the user opens the application after having authenticated in the past. In this case, we should already have the token in persisted
22storage. We fetch the token from storage and add to each request, usually as an auth header.
23
24**Forced log out due to invalid token** — the user's session could become invalid for a variety reasons: their token expired, they requested to be
25signed out of all devices, or their session was invalidated remotely. In this case, we would want to
26also log them out in the application, so they
27could have the opportunity to log in again. To do this, we want to clear any persisted storage, and redirect them to the application home or login page.
28
29**User initiated log out** — when the user chooses to log out of the application, we usually send a logout request to the API, then clear any tokens
30from persisted storage, and redirect them to the application home or login page.
31
32**Refresh (optional)** — this is not always implemented; if your API supports it, the
33user will receive both an auth token, and a refresh token.
34The auth token is usually valid for a shorter duration of time (e.g. 1 week) than the refresh token
35(e.g. 6 months), and the latter can be used to request a new
36auth token if the auth token has expired. The refresh logic is triggered either when the JWT is known to be invalid (e.g. by decoding it and inspecting the expiry date),
37or when an API request returns with an unauthorized response. For graphQL APIs, it is usually an error code, instead of a 401 HTTP response, but both can be supported.
38When the token has been successfully refreshed (this can be done as a mutation to the graphQL API or a request to a different API endpoint, depending on implementation),
39we will save the new token in persisted storage, and retry the failed request with the new auth header. The user should be logged out and persisted storage cleared if
40the refresh fails or if the re-executing the query with the new token fails with an auth error for the second time.
41
42## Installation & Setup
43
44First, install the `@urql/exchange-auth` alongside `urql`:
45
46```sh
47yarn add @urql/exchange-auth
48# or
49npm install --save @urql/exchange-auth
50```
51
52You'll then need to add the `authExchange`, that this package exposes to your `Client`. The `authExchange` is an asynchronous exchange, so it must be placed
53in front of all `fetchExchange`s but after all other synchronous exchanges, like the `cacheExchange`.
54
55```js
56import { Client, cacheExchange, fetchExchange } from 'urql';
57import { authExchange } from '@urql/exchange-auth';
58
59const client = new Client({
60 url: 'http://localhost:3000/graphql',
61 exchanges: [
62 cacheExchange,
63 authExchange(async utils => {
64 return {
65 /* config... */
66 };
67 }),
68 fetchExchange,
69 ],
70});
71```
72
73You pass an initialization function to the `authExchange`. This function is called by the exchange
74when it first initializes. It'll let you receive an object of utilities and you must return
75a (promisified) object of configuration options.
76
77Let's discuss each of the [configuration options](../api/auth-exchange.md#options) and how to use them in turn.
78
79### Configuring the initializer function (initial load)
80
81The initializer function must return a promise of a configuration object and hence also gives you an
82opportunity to fetch your authentication state from storage.
83
84```js
85async function initializeAuthState() {
86 const token = localStorage.getItem('token');
87 const refreshToken = localStorage.getItem('refreshToken');
88 return { token, refreshToken };
89}
90
91authExchange(async utils => {
92 let { token, refreshToken } = initializeAuthState();
93 return {
94 /* config... */
95 };
96});
97```
98
99The first step here is to retrieve our tokens from a kind of storage, which may be asynchronous as
100well, as illustrated by `initializeAuthState`.
101
102In React Native, this is very similar, but because persisted storage in React Native is always
103asynchronous and promisified, we would await our tokens. This works because the
104function that `authExchange` is async, i.e. must return a `Promise`.
105
106```js
107async function initializeAuthState() {
108 const token = await AsyncStorage.getItem(TOKEN_KEY);
109 const refreshToken = await AsyncStorage.getItem(REFRESH_KEY);
110 return { token, refreshToken };
111}
112
113authExchange(async utils => {
114 let { token, refreshToken } = initializeAuthState();
115 return {
116 /* config... */
117 };
118});
119```
120
121### Configuring `addAuthToOperation`
122
123The purpose of `addAuthToOperation` is to apply an auth state to each request. Here, we'll use the
124tokens we retrieved from storage and add them to our operations.
125
126In this example, we're using a utility we're passed, `appendHeaders`. This utility is a simply
127shortcut to quickly add HTTP headers via `fetchOptions` to an `Operation`, however, we may as well
128be editing the `Operation` context here using `makeOperation`.
129
130```js
131authExchange(async utils => {
132 let token = await AsyncStorage.getItem(TOKEN_KEY);
133 let refreshToken = await AsyncStorage.getItem(REFRESH_KEY);
134
135 return {
136 addAuthToOperation(operation) {
137 if (!token) return operation;
138 return utils.appendHeaders(operation, {
139 Authorization: `Bearer ${token}`,
140 });
141 },
142 // ...
143 };
144});
145```
146
147First, we check that we have a non-null `token`. Then we apply it to the request using the
148`appendHeaders` utility as an `Authorization` header.
149
150We could also be using `makeOperation` here to update the context in any other way, such as:
151
152```js
153import { makeOperation } from '@urql/core';
154
155makeOperation(operation.kind, operation, {
156 ...operation.context,
157 someAuthThing: token,
158});
159```
160
161### Configuring `didAuthError`
162
163This function lets the `authExchange` know what is defined to be an API error for your API.
164`didAuthError` is called by `authExchange` when it receives an `error` on an `OperationResult`, which
165is of type [`CombinedError`](../api/core.md#combinederror).
166
167We can for example check the error's `graphQLErrors` array in `CombinedError` to determine if an auth
168error has occurred. While your API may implement this differently, an authentication error on an
169execution result may look a little like this if your API uses `extensions.code` on errors:
170
171```js
172{
173 data: null,
174 errors: [
175 {
176 message: 'Unauthorized: Token has expired',
177 extensions: {
178 code: 'FORBIDDEN'
179 },
180 }
181 ]
182}
183```
184
185If you're building a new API, using `extensions` on errors is the recommended approach to add
186metadata to your errors. We'll be able to determine whether any of the GraphQL errors were due
187to an unauthorized error code, which would indicate an auth failure:
188
189```js
190authExchange(async utils => {
191 // ...
192 return {
193 // ...
194 didAuthError(error, _operation) {
195 return error.graphQLErrors.some(e => e.extensions?.code === 'FORBIDDEN');
196 },
197 };
198});
199```
200
201For some GraphQL APIs, the authentication error is only communicated via a 401 HTTP status as is
202common in RESTful APIs, which is suboptimal, but which we can still write a check for.
203
204```js
205authExchange(async utils => {
206 // ...
207 return {
208 // ...
209 didAuthError(error, _operation) {
210 return error.response?.status === 401;
211 },
212 };
213});
214```
215
216If `didAuthError` returns `true`, it will trigger the `authExchange` to trigger the logic for asking
217for re-authentication via `refreshAuth`.
218
219### Configuring `refreshAuth` (triggered after an auth error has occurred)
220
221If the API doesn't support any sort of token refresh, this is where we could simply log the user out.
222
223```js
224authExchange(async utils => {
225 // ...
226 return {
227 // ...
228 async refreshAuth() {
229 logout();
230 },
231 };
232});
233```
234
235Here, `logout()` is a placeholder that is called when we got an error, so that we can redirect to a
236login page again and clear our tokens from local storage or otherwise.
237
238If we had a way to refresh our token using a refresh token, we can attempt to get a new token for the
239user first:
240
241```js
242authExchange(async utils => {
243 let token = localStorage.getItem('token');
244 let refreshToken = localStorage.getItem('refreshToken');
245
246 return {
247 // ...
248 async refreshAuth() {
249 const result = await utils.mutate(REFRESH, { refreshToken });
250
251 if (result.data?.refreshLogin) {
252 // Update our local variables and write to our storage
253 token = result.data.refreshLogin.token;
254 refreshToken = result.data.refreshLogin.refreshToken;
255 localStorage.setItem('token', token);
256 localStorage.setItem('refreshToken', refreshToken);
257 } else {
258 // This is where auth has gone wrong and we need to clean up and redirect to a login page
259 localStorage.clear();
260 logout();
261 }
262 },
263 };
264});
265```
266
267Here we use the special `mutate` utility method provided by the `authExchange` to do the token
268refresh. This is a useful method to use if your GraphQL API expects you to make a GraphQL mutation
269to update your authentication state. It will send the mutation and bypass all authentication and
270prior exchanges.
271
272If your authentication is not handled via GraphQL but a REST endpoint, you can use the `fetch` API
273here however instead of a mutation.
274
275All other requests will be paused while `refreshAuth` runs, so we won't have to deal with multiple
276authentication errors or refreshes at once.
277
278### Configuring `willAuthError`
279
280`willAuthError` is an optional parameter and is run _before_ a request is made.
281
282We can use it to trigger an authentication error and let the `authExchange` run our `refreshAuth`
283function without the need to first let a request fail with an authentication error. For example, we
284can use this to predict an authentication error, for instance, because of expired JWT tokens.
285
286```js
287authExchange(async utils => {
288 // ...
289 return {
290 // ...
291 willAuthError(_operation) {
292 // Check whether `token` JWT is expired
293 return false;
294 },
295 };
296});
297```
298
299This can be really useful when we know when our authentication state is invalid and want to prevent
300even sending any operation that we know will fail with an authentication error.
301
302However, we have to be careful on how we define this function, if some queries or login mutations
303are sent to our API without being logged in. In these cases, it's better to either detect the
304mutations we'd like to allow or return `false` when a token isn't set in storage yet.
305
306If we'd like to detect a mutation that will never fail with an authentication error, we could for
307instance write the following logic:
308
309```js
310authExchange(async utils => {
311 // ...
312 return {
313 // ...
314 willAuthError(operation) {
315 if (
316 operation.kind === 'mutation' &&
317 // Here we find any mutation definition with the "login" field
318 operation.query.definitions.some(definition => {
319 return (
320 definition.kind === 'OperationDefinition' &&
321 definition.selectionSet.selections.some(node => {
322 // The field name is just an example, since signup may also be an exception
323 return node.kind === 'Field' && node.name.value === 'login';
324 })
325 );
326 })
327 ) {
328 return false;
329 } else if (false /* is JWT expired? */) {
330 return true;
331 } else {
332 return false;
333 }
334 },
335 };
336});
337```
338
339Alternatively, you may decide to let all operations through if your token isn't set in storage, i.e.
340if you have no prior authentication state.
341
342## Handling Logout by reacting to Errors
343
344We can also handle authentication errors in a `mapExchange` instead of the `authExchange`.
345To do this, we'll need to add the `mapExchange` to the exchanges array, _before_ the `authExchange`.
346The order is very important here:
347
348```js
349import { createClient, cacheExchange, fetchExchange, mapExchange } from 'urql';
350import { authExchange } from '@urql/exchange-auth';
351
352const client = createClient({
353 url: 'http://localhost:3000/graphql',
354 exchanges: [
355 cacheExchange,
356 mapExchange({
357 onError(error, _operation) {
358 const isAuthError = error.graphQLErrors.some(e => e.extensions?.code === 'FORBIDDEN');
359 if (isAuthError) {
360 logout();
361 }
362 },
363 }),
364 authExchange(async utils => {
365 return {
366 /* config */
367 };
368 }),
369 fetchExchange,
370 ],
371});
372```
373
374The `mapExchange` will only receive an auth error when the auth exchange has already tried and failed
375to handle it. This means we have either failed to refresh the token, or there is no token refresh
376functionality. If we receive an auth error in the `mapExchange`'s `onError` function
377(as defined in the `didAuthError` configuration section above), then we can be confident that it is
378an authentication error that the `authExchange` isn't able to recover from, and the user should be
379logged out.
380
381## Cache Invalidation on Logout
382
383If we're dealing with multiple authentication states at the same time, e.g. logouts, we need to
384ensure that the `Client` is reinitialized whenever the authentication state changes.
385Here's an example of how we may do this in React if necessary:
386
387```jsx
388import { createClient, Provider } from 'urql';
389
390const App = ({ isLoggedIn }: { isLoggedIn: boolean | null }) => {
391 const client = useMemo(() => {
392 if (isLoggedIn === null) {
393 return null;
394 }
395
396 return createClient({ /* config */ });
397 }, [isLoggedIn]);
398
399 if (!client) {
400 return null;
401 }
402
403 return {
404 <Provider value={client}>
405 {/* app content */}
406 <Provider>
407 }
408}
409```
410
411When the application launches, the first thing we do is check whether the user has any authentication
412tokens in persisted storage. This will tell us whether to show the user the logged in or logged out view.
413
414The `isLoggedIn` prop should always be updated based on authentication state change. For instance, we may set it to
415`true` after the user has authenticated and their tokens have been added to storage, and set it to
416`false` once the user has been logged out and their tokens have been cleared. It's important to clear
417or add tokens to a storage _before_ updating the prop in order for the auth exchange to work
418correctly.
419
420This pattern of creating a new `Client` when changing authentication states is especially useful
421since it will also recreate our client-side cache and invalidate all cached data.