Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
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.