Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

(docs) - Add page for more detailed auth docs (#973)

* Add more detailed documentation for authentication

* Add a section on using errorExchange with authExcahnge

* Apply suggestions from code review

Co-authored-by: Phil Pluckthun <phil@kitten.sh>

* Add suggestions from code review

* Move cache invalidation/logout docs to the bottom of the page

* Update documentation for didAuthError

Co-authored-by: Phil Pluckthun <phil@kitten.sh>

authored by

Kadi Kraman
Phil Pluckthun
and committed by
GitHub
6d4e4012 f44d7163

+358 -2
+1
docs/advanced/README.md
··· 18 18 devtools](https://github.com/FormidableLabs/urql-devtools/) and how to add our own debug events 19 19 for its event view. 20 20 - [**Retrying operations**](./retry-operations.md) shows the `retryExchange` which allows you to retry operations when they've failed. 21 + - [**Authentication**](./authentication.md) describes how to implement authentication using the `authExchange` 21 22 - [**Testing**](./testing.md) covers how to test components that use `urql` particularly in React. 22 23 - [**Auto-populate Mutations**](./auto-populate-mutations.md) presents the `populateExchange` addon which can make it easier to 23 24 update normalized data after mutations.
+355
docs/advanced/authentication.md
··· 1 + --- 2 + title: Authentication 3 + order: 6 4 + --- 5 + 6 + # Authentication 7 + 8 + Most APIs include some type of authentication, usually in the form of an auth token that is sent with each request header. 9 + 10 + The purpose of the [`authExchange`](../api/auth-exchange.md) is to provide a flexible API that facilitates the typical 11 + JWT-based authentication flow. 12 + 13 + ## Typical Authentication Flow 14 + 15 + **Initial login** - the user opens the application and authenticates for the first time. They enter their credentials and receive an auth token. 16 + The token is saved to storage that is persisted though sessions, e.g. `localStorage` on the web or `AsyncStorage` in React Native. The token is 17 + added to each subsequent request in an auth header. 18 + 19 + **Resume** - the user opens the application after having authenticated in the past. In this case, we should already have the token in persisted 20 + storage. We fetch the token from storage and add to each request, usually as an auth header. 21 + 22 + **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 23 + signed out of all devices, or their session was invalidated remotely. In this case, we would want to also log them out in the application so they 24 + could 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. 25 + 26 + **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 27 + from persisted storage, and redirect them to the application home or login page. 28 + 29 + **Refresh (optional)** - this is not always implemented, but given that your API supports it, the user will receive both an auth token and a refresh token, 30 + where the auth token is valid for a shorter duration of time (e.g. 1 week) than the refresh token (e.g. 6 months) and the latter can be used to request a new 31 + auth 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), 32 + or 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. 33 + When the token as 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), 34 + we 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 35 + the refresh fails or if the re-executing the query with the new token fails with an auth error for the second time. 36 + 37 + ## Installation & Setup 38 + 39 + First, install the `@urql/exchange-auth` alongside `urql`: 40 + 41 + ```sh 42 + yarn add @urql/exchange-auth 43 + # or 44 + npm install --save @urql/exchange-auth 45 + ``` 46 + 47 + You'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 48 + in front of all `fetchExchange`s but after all other synchronous exchanges, like the `cacheExchange`. 49 + 50 + ```js 51 + import { createClient, dedupExchange, cacheExchange, fetchExchange } from 'urql'; 52 + import { authExchange } from '@urql/exchange-auth'; 53 + 54 + const client = createClient({ 55 + url: '/graphql', 56 + exchanges: [ 57 + dedupExchange, 58 + cacheExchange, 59 + authExchange({ 60 + /* config */ 61 + }), 62 + fetchExchange, 63 + ], 64 + }); 65 + ``` 66 + 67 + Let's discuss each of the [configuration options](../api/auth-exchange/#options) and how to use them in turn. 68 + 69 + ### Configuring `getAuth` (initial load, fetch from storage) 70 + 71 + The `getAuth` option is used to fetch the auth state. This is how to configure it for fetching the tokens at initial launch in React: 72 + 73 + ```js 74 + const getAuth = async ({ authState }) => { 75 + if (!authState) { 76 + const token = localStorage.getItem('token'); 77 + const refreshToken = localStorage.getItem('refreshToken'); 78 + if (token && refreshToken) { 79 + return { token, refreshToken }; 80 + } 81 + return null; 82 + } 83 + 84 + return null; 85 + } 86 + ``` 87 + 88 + We check that the `authState` doesn't already exist (this indicates that it is the first time this exchange is executed and not an auth failure) and fetch the auth state from 89 + storage. The structure of this particular`authState` is an object with keys for `token` and `refreshToken`, but this format is not required. You can 90 + use different keys or store any additional auth related information here. For example you could decode and store the token expiry date, which would save you from decoding 91 + your JWT every time you want to check whether your token is expired. 92 + 93 + In React Native, this is very similar, but because persisted storage in React Native is always asynchronous, so is this function: 94 + 95 + ```js 96 + const getAuth = async ({ authState, mutate }) => { 97 + if (!authState) { 98 + const token = await AsyncStorage.getItem(TOKEN_KEY, {}); 99 + const refreshToken = await AsyncStorage.getItem(REFRESH_TOKEN_KEY, {}); 100 + if (token && refreshToken) { 101 + return { token, refreshToken }; 102 + } 103 + return null; 104 + } 105 + 106 + return null; 107 + } 108 + ``` 109 + 110 + ### Configuring `addAuthToOperation` 111 + 112 + The purpose of `addAuthToOperation` is to take apply your auth state to each request. Note that the format of the `authState` will be whatever 113 + you've returned from `getAuth` and not at all constrained by the exchange: 114 + 115 + ```js 116 + const addAuthToOperation = ({ 117 + authState, 118 + operation, 119 + }) => { 120 + if (!authState || !authState.token) { 121 + return operation; 122 + } 123 + 124 + const fetchOptions = 125 + typeof operation.context.fetchOptions === 'function' 126 + ? operation.context.fetchOptions() 127 + : operation.context.fetchOptions || {}; 128 + 129 + return { 130 + ...operation, 131 + context: { 132 + ...operation.context, 133 + fetchOptions: { 134 + ...fetchOptions, 135 + headers: { 136 + ...fetchOptions.headers, 137 + "Authorization": authState.token, 138 + }, 139 + }, 140 + }, 141 + }; 142 + } 143 + ``` 144 + 145 + First we check that we have an `authState` and a `token`. Then we apply it to the request `fetchOptions` as an `Authorization` header. 146 + The header format can vary based on the API (e.g using `Bearer ${token}` instead of just `token`) which is why it'll be up to you to add the header 147 + in the expected format for your API. 148 + 149 + ### Configuring `didAuthError` 150 + 151 + This function lets the exchange know what is defined to be an API error for your API. `didAuthError` receives an `error` which is of type 152 + [`CombinedError`](../api/core.md#combinederror) and we can use the `graphQLErrors` array in `CombinedError` to determine if an auth error has occurred. 153 + 154 + The GraphQL error looks like something like this: 155 + 156 + ```js 157 + { 158 + data: null, 159 + errors: [ 160 + { 161 + message: 'Unauthorized: Token has expired', 162 + extensions: { 163 + code: 'FORBIDDEN' 164 + }, 165 + response: { 166 + status: 200 167 + } 168 + ] 169 + } 170 + ``` 171 + 172 + Most GraphQL APIs will communicate auth errors via the [error code extension](https://www.apollographql.com/docs/apollo-server/data/errors/#codes) which 173 + is the recommended approach. We'll be able to determine whether any of the GraphQL errors were due to an unauthorized error code, which would indicate an auth failure: 174 + 175 + ```js 176 + const didAuthError = ({ error }) => { 177 + return error.graphQLErrors.some( 178 + e => e.extensions?.code === 'FORBIDDEN', 179 + ); 180 + } 181 + ``` 182 + 183 + For some GraphQL APIs, the auth error is communicated via an 401 HTTP response as is common in RESTful APIs: 184 + 185 + ```js 186 + { 187 + data: null, 188 + errors: [ 189 + { 190 + message: 'Unauthorized: Token has expired', 191 + response: { 192 + status: 401 193 + } 194 + ] 195 + } 196 + ``` 197 + 198 + In this case we can determine the auth error based on the status code of the request: 199 + 200 + ```js 201 + const didAuthError = ({ error }) => { 202 + return error.graphQLErrors.some( 203 + e => e.response.status === 401, 204 + ); 205 + }, 206 + ``` 207 + 208 + If `didAuthError` returns `true`, it will trigger the exchange to trigger the logic for asking for re-authentication via `getAuth`. 209 + 210 + ### Configuring `getAuth` (triggered after an auth error has occurred) 211 + 212 + If your API doesn't support any sort of token refresh, this is where you should simply log the user out. 213 + 214 + ```js 215 + const getAuth = async ({ authState }) => { 216 + if (!authState) { 217 + const token = localStorage.getItem('token'); 218 + const refreshToken = localStorage.getItem('refreshToken'); 219 + if (token && refreshToken) { 220 + return { token, refreshToken }; 221 + } 222 + return null; 223 + } 224 + 225 + logout(); 226 + 227 + return null; 228 + } 229 + ``` 230 + 231 + Here, `logout()` is a placeholder that is called when we got an error, so that we can redirect to a login page again and clear our tokens from local storage or otherwise. 232 + 233 + If we had a way to refresh our token using a refresh token, we can attempt to get a new token for the user first: 234 + 235 + ```js 236 + const getAuth = async ({ authState, mutate }) => { 237 + if (!authState) { 238 + const token = localStorage.getItem('token'); 239 + const refreshToken = localStorage.getItem('refreshToken'); 240 + if (token && refreshToken) { 241 + return { token, refreshToken }; 242 + } 243 + return null; 244 + } 245 + 246 + const result = await mutate(refreshMutation, { 247 + token: authState!.refreshToken, 248 + }); 249 + 250 + if (result.data?.refreshLogin) { 251 + localStorage.setItem('token', result.data.refreshLogin.token); 252 + localStorage.setItem('refreshToken', result.data.refreshLogin.refreshToken); 253 + 254 + return { 255 + token: result.data.refreshLogin.token, 256 + refreshToken: result.data.refreshLogin.refreshToken, 257 + }; 258 + } 259 + 260 + // This is where auth has gone wrong and we need to clean up and redirect to a login page 261 + localStorage.clear(); 262 + logout(); 263 + 264 + return null; 265 + } 266 + ``` 267 + 268 + Here we use the special mutate function provided by the auth exchange to do the token refresh. If your auth is not handled via GraphQL but a REST endpoint, you can 269 + use `fetch` in this function instead of a mutation. All other requests will be paused while `getAuth` returns, so we never have to handle multiple auth failures 270 + at the same time. 271 + 272 + ### Configuring `willAuthError` 273 + 274 + `willAuthError` is an optional parameter and is run _before_ a network request is made. We can use it to trigger the logic in 275 + `getAuth` without the need to send a request and get a GraphQL Error back. For example, we can use this to predict that the authentication will fail because our JWT is invalid already: 276 + 277 + ```js 278 + const willAuthError = ({ authState }) => { 279 + if (!authState || /* JWT is expired */) return true; 280 + return false; 281 + } 282 + ``` 283 + 284 + [Read more about `@urql/exchange-auth`'s API in our API docs.](../api/auth-exchange.md) 285 + 286 + ## Handling Logout with the Error Exchange 287 + 288 + We can also handle authentication errors in an `errorExchange` instead of the `authExchange`. To do this, we'll need to add the 289 + `errorExchange` to the exchanges array, _before_ the `authExchange`. The order is very important here: 290 + 291 + ```js 292 + import { createClient, dedupExchange, cacheExchange, fetchExchange, errorExchange } from 'urql'; 293 + import { authExchange } from '@urql/exchange-auth'; 294 + 295 + const client = createClient({ 296 + url: '/graphql', 297 + exchanges: [ 298 + dedupExchange, 299 + cacheExchange, 300 + errorExchange({ 301 + onError: error => { 302 + const isAuthError = error.graphQLErrors.some( 303 + e => e.extensions?.code === 'FORBIDDEN', 304 + ); 305 + 306 + if (isAuthError) { 307 + logout(); 308 + } 309 + } 310 + }), 311 + authExchange({ 312 + /* config */ 313 + }), 314 + fetchExchange, 315 + ], 316 + }); 317 + ``` 318 + 319 + The `errorExchange` will only receive an auth error when the auth exchange has already tried and failed to handle it. This means we have 320 + either failed to refresh the token, or there is no token refresh functionality. If we receive an auth error in the `errorExchange` (as defined in 321 + the `didAuthError` configuration section above), then we can be confident that it is an auth error that the `authExchange` isn't able to recover 322 + from, and the user should be logged out. 323 + 324 + ## Cache Invalidation on Logout 325 + 326 + If we're dealing with multiple authentication states at the same time, e.g. logouts, we need to ensure that the `Client` is reinitialized whenever the authentication state changes. Here's an example of how we may do this in React if necessary: 327 + 328 + ```js 329 + const App = ({ isLoggedIn }: { isLoggedIn: boolean | null }) => { 330 + const client = useMemo(() => { 331 + if (isLoggedIn === null) { 332 + return null; 333 + } 334 + 335 + return createClient({ /* config */ }); 336 + }, [isLoggedIn]); 337 + 338 + if (!client) { 339 + return null; 340 + } 341 + 342 + return { 343 + <GraphQLProvider value={client}> 344 + {/* app content */} 345 + <GraphQLProvider> 346 + } 347 + } 348 + ``` 349 + 350 + When the application launches, the first thing we do is check whether the user has any auth tokens in persisted storage. This will tell us 351 + whether to show the user the logged in or logged out view. 352 + 353 + The `isLoggedIn` prop should always be updated based on authentication state change e.g. set to `true` after the use has authenticated and their tokens have been 354 + added to storage, and set to `false` if the user has been logged out and their tokens have been cleared. It's important clear or add tokens to storage _before_ 355 + updating the prop in order for the auth exchange to work correctly.
+1 -1
docs/advanced/auto-populate-mutations.md
··· 1 1 --- 2 2 title: Auto-populate Mutations 3 - order: 7 3 + order: 8 4 4 --- 5 5 6 6 # Automatically populating Mutations
+1 -1
docs/advanced/testing.md
··· 1 1 --- 2 2 title: Testing 3 - order: 6 3 + order: 7 4 4 --- 5 5 6 6 # Testing