Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(exchanges) implement auth exchange #939

Merged
merged 20 commits into from
Sep 7, 2020
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions exchanges/auth/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## v0.1.0

**Initial Release**
163 changes: 163 additions & 0 deletions exchanges/auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<h2 align="center">@urql/exchange-auth</h2>

<p align="center"><strong>An exchange for managing authentication in <code>urql</code></strong></p>

`@urql/exchange-auth` is an exchange for the [`urql`](https://github.com/FormidableLabs/urql) GraphQL client which helps handle auth headers and token refresh

## Quick Start Guide

First install `@urql/exchange-auth` alongside `urql`:

```sh
yarn add @urql/exchange-auth
# or
npm install --save @urql/exchange-auth
```

You'll then need to add the `authExchange`, that this package exposes to your `urql` Client

```js
import { createClient, dedupExchange, cacheExchange, Operation } from 'urql';
import { executeExchange } from '@urql/exchange-execute';

/**
* your auth state can be anything
* this is a generic example with a token and refreshToken, but it can
* include token expiry for example if you want to check that in willAuthError
* without having to decode the token every time
**/

type AuthState = {
token: String;
refreshToken: String;
} | null;

const client = createClient({
url: 'http://localhost:1234/graphql',
exchanges: [
dedupExchange,
cacheExchange,
authExchange({
addAuthToOperation: ({
authState,
operation,
}: {
authState: AuthState;
operation: Operation;
}): Operation => {
// the token isn't in the auth state, return the operation without changes
if (!authState || !authState.token) {
return operation;
}

const { token } = authState;

const authHeader = {
Authorization: token,
};

const fetchOptions =
typeof operation.context.fetchOptions === 'function'
? operation.context.fetchOptions()
: operation.context.fetchOptions || {};

return {
...operation,
context: {
...operation.context,
fetchOptions: {
...fetchOptions,
headers: {
...fetchOptions.headers,
...authHeader,
},
},
},
};
},
willAuthError: ({ authState }) => {
if (!authState) return true;
// e.g. check for expiration, existence of auth etc
return false;
},
didAuthError: ({ error }) => {
// check if the error was an auth error (this can be implemented in various ways, e.g. 401 or a special error code)
return error.graphQLErrors.some(
e => e.extensions?.code === 'FORBIDDEN',
);
},
getAuth: async ({ authState, mutate }) => {
// for initial launch, fetch the auth state from storage (local storage, async storage etc)
if (!authState) {
const token = localStorage.getItem('token');
const refreshToken = localStorage.getItem('refreshToken');
if (token && refreshToken) {
return { token, refreshToken };
}
return null;
}

/**
* the following code gets exetuted when an auth error has occurred
* we should refresh the token if possible and return a new auth state
* If refrsh fails, we should log out
**/

// if your refresh logic is in graphQL, you must use this mutate function to call it
// if your refresh logic is a separate RESTful endpoint, use fetch or similar
const result = await mutate(refreshMutation, {
token: authState!.refreshToken,
});

if (result.data?.refreshLogin) {
// save the new tokens in storage for next restart
localStorage.setItem('token', result.data.refreshLogin.token);
localStorage.setItem('refreshToken', result.data.refreshLogin.refreshToken);

// return the new tokens
return {
token: result.data.refreshLogin.token,
refreshToken: result.data.refreshLogin.refreshToken,
};
}

// otherwise, if refresh fails, log clear storage and log out
localStorage.clear();

// your app logout logic should trigger here
logout();

return null;
},
}),
fetchExchange
],
});
```

## Handling rrrors via the errorExchange

Handling the logout logic in `getAuth` is the easiest way to get started, but it means the errors will always get swallowed by the `authExchange`.
If you want to handle errors globally, this can be done using the `errorExchange`:

```js
import { errorExchange } from 'urql';

// this needs to be placed ABOVE the authExchange in the exchanges array, otherwise the auth error will show up hear before the auth exchange has had the chance to handle it
errorExchange({
onError: ({ error }) => {
// we only get an auth error here when the auth excahnge had attempted to refresh auth and getting an auth error again for the second time
const isAuthError = error.graphQLErrors.some(
e => e.extensions?.code === 'FORBIDDEN',
);

if (isAuthError) {
// clear storage, log the user out etc
}
}
}),
```

## Maintenance Status

**Active:** Formidable is actively working on this project, and we expect to continue for work for the foreseeable future. Bug reports, feature requests and pull requests are welcome.
67 changes: 67 additions & 0 deletions exchanges/auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"name": "@urql/exchange-auth",
"version": "0.1.0",
"description": "An exchange for managing authentication and token refresh in urql",
"sideEffects": false,
"homepage": "https://formidable.com/open-source/urql/docs/",
"bugs": "https://github.com/FormidableLabs/urql/issues",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/FormidableLabs/urql.git",
"directory": "exchanges/auth"
},
"keywords": [
"urql",
"exchange",
"auth",
"authentication",
"formidablelabs",
"exchanges"
],
"main": "dist/urql-exchange-auth",
"module": "dist/urql-exchange-auth.mjs",
"types": "dist/types/index.d.ts",
"source": "src/index.ts",
"exports": {
".": {
"import": "./dist/urql-exchange-auth.mjs",
"require": "./dist/urql-exchange-auth.js",
"types": "./dist/types/index.d.ts",
"source": "./src/index.ts"
},
"./package.json": "./package.json"
},
"files": [
"LICENSE",
"CHANGELOG.md",
"README.md",
"dist/"
],
"scripts": {
"test": "jest",
"clean": "rimraf dist extras",
"check": "tsc --noEmit",
"lint": "eslint --ext=js,jsx,ts,tsx .",
"build": "rollup -c ../../scripts/rollup/config.js",
"prepare": "node ../../scripts/prepare/index.js",
"prepublishOnly": "run-s clean build"
},
"jest": {
"preset": "../../scripts/jest/preset"
},
"dependencies": {
"@urql/core": ">=1.12.0",
"wonka": "^4.0.14"
},
"peerDependencies": {
"graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
},
"devDependencies": {
"graphql": "^15.1.0",
"graphql-tag": "^2.10.1"
},
"publishConfig": {
"access": "public"
}
}
Loading