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

Introduce withRefreshAuth #8574

Merged
merged 6 commits into from
Feb 24, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
85 changes: 85 additions & 0 deletions docs/addRefreshAuthToAuthProvider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
layout: default
title: "addRefreshAuthToAuthProvider"
---

# `addRefreshAuthToAuthProvider`

This helper function wraps an existing [`authProvider`]('./Authentication.md') to support authentication token refreshing mechanisms.

## Usage

Use `addRefreshAuthToAuthProvider` to decorate an existing auth provider. In addition to the base provider, this function takes a function responsible for refreshing the authentication token if needed.

Here is a simple example that refreshes an expired JWT token when needed:

```jsx
// in src/refreshAuth.js
import { getAuthTokensFromLocalStorage } from './getAuthTokensFromLocalStorage';
import { refreshAuthTokens } from './refreshAuthTokens';

export const refreshAuth = () => {
const { accessToken, refreshToken } = getAuthTokensFromLocalStorage();
if (accessToken.exp < Date.now().getTime() / 1000) {
// This function will fetch the new tokens from the authentication service and update them in localStorage
return refreshAuthTokens(refreshToken);
}
return Promise.resolve();
}

// in src/authProvider.js
import { addRefreshAuthToAuthProvider } from 'react-admin';
import { refreshAuth } from 'refreshAuth';

const myAuthProvider = {
// ...Usual AuthProvider methods
};

export const authProvider = addRefreshAuthToAuthProvider(myAuthProvider, refreshAuth);
```

Then, inject the decorated provider in the `<Admin>` component:

```jsx
// in src/App.js
import { Admin } from 'react-admin';
import { dataProvider } from './dataProvider';
import { authProvider } from './authProvider';

export const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider}>
{/* ... */}
</Admin>
)
```

**Tip:** We usually wrap the data provider's methods in the same way. You can use the [`addRefreshAuthToDataProvider`](./addRefreshAuthToDataProvider.md) helper function to do so.

## `provider`

The first argument must be a valid `authProvider` object - for instance, [any third-party auth provider](./AuthProviderList.md).

```jsx
// in src/authProvider.js
import { addRefreshAuthToAuthProvider } from 'react-admin';

const myAuthProvider = {
// ...Usual AuthProvider methods
};

export const authProvider = addRefreshAuthToAuthProvider(myAuthProvider, [ /* refreshAuth function */ ]);
```

## `refreshAuth`

The second argument is a function responsible for refreshing the authentication tokens if needed. It must return a promise.

```jsx
import { refreshAuth } from "./refreshAuth";

export const authProvider = addRefreshAuthToAuthProvider(myAuthProvider, refreshAuth);
```

## See Also

- [`addRefreshAuthToDataProvider`](./addRefreshAuthToDataProvider.md)
83 changes: 83 additions & 0 deletions docs/addRefreshAuthToDataProvider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
layout: default
title: "addRefreshAuthToDataProvider"
---

# `addRefreshAuthToDataProvider`

This helper function wraps an existing [`dataProvider`](./DataProviderIntroduction.md) to support authentication token refreshing mechanisms.

## Usage

Use `addRefreshAuthToDataProvider` to decorate an existing data provider. In addition to the base provider, this function takes a function responsible for refreshing the authentication token if needed.

Here is a simple example that refreshes an expired JWT token when needed:

```jsx
// in src/refreshAuth.js
import { getAuthTokensFromLocalStorage } from './getAuthTokensFromLocalStorage';
import { refreshAuthTokens } from './refreshAuthTokens';

export const refreshAuth = () => {
const { accessToken, refreshToken } = getAuthTokensFromLocalStorage();
if (accessToken.exp < Date.now().getTime() / 1000) {
// This function will fetch the new tokens from the authentication service and update them in localStorage
return refreshAuthTokens(refreshToken);
}
return Promise.resolve();
}

// in src/dataProvider.js
import { addRefreshAuthToDataProvider } from 'react-admin';
import simpleRestProvider from 'ra-data-simple-rest';
import { refreshAuth } from 'refreshAuth';

const baseDataProvider = simpleRestProvider('http://path.to.my.api/');

export const dataProvider = addRefreshAuthToDataProvider(baseDataProvider, refreshAuth);
```

Then, inject the decorated provider in the `<Admin>` component:

```jsx
// in src/App.js
import { Admin } from 'react-admin';
import { dataProvider } from './dataProvider';

export const App = () => (
<Admin dataProvider={dataProvider}>
{/* ... */}
</Admin>
)
```

**Tip:** We usually wrap the auth provider's methods in the same way. You can use the [`addRefreshAuthToAuthProvider`](./addRefreshAuthToAuthProvider.md) helper function to do so.

## `provider`

The first argument must be a valid `dataProvider` object - for instance, [any third-party data provider](./DataProviderList.md).

```jsx
// in src/dataProvider.js
import { addRefreshAuthToDataProvider } from 'react-admin';
import simpleRestProvider from 'ra-data-simple-rest';

const baseDataProvider = simpleRestProvider('http://path.to.my.api/');

export const dataProvider = addRefreshAuthToDataProvider(baseDataProvider, [ /* refreshAuth function */ ]);
```

## `refreshAuth`

The second argument is a function responsible for refreshing the authentication tokens if needed. It must return a promise.

```jsx
import jsonServerProvider from "ra-data-json-server";
import { refreshAuth } from "./refreshAuth";

export const dataProvider = addRefreshAuthToDataProvider(baseDataProvider, refreshAuth);
```

## See Also

- [`addRefreshAuthToAuthProvider`](./addRefreshAuthToAuthProvider.md)
2 changes: 2 additions & 0 deletions docs/navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
<li {% if page.path == 'usePermissions.md' %} class="active" {% endif %}><a class="nav-link" href="./usePermissions.html"><code>usePermissions</code></a></li>
<li {% if page.path == 'useCanAccess.md' %} class="active" {% endif %}><a class="nav-link" href="./useCanAccess.html"><code>useCanAccess</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'canAccess.md' %} class="active" {% endif %}><a class="nav-link" href="./canAccess.html"><code>canAccess</code><img class="premium" src="./img/premium.svg" /></a></li>
<li {% if page.path == 'addRefreshAuthToAuthProvider.md' %} class="active" {% endif %}><a class="nav-link" href="./addRefreshAuthToAuthProvider.html"><code>addRefreshAuthToAuthProvider</code></a></li>
<li {% if page.path == 'addRefreshAuthToDataProvider.md' %} class="active" {% endif %}><a class="nav-link" href="./addRefreshAuthToDataProvider.html"><code>addRefreshAuthToDataProvider</code></a></li>
</ul>

<ul><div>List Page</div>
Expand Down
51 changes: 51 additions & 0 deletions packages/ra-core/src/auth/addRefreshAuthToAuthProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { AuthProvider } from '../types';

/**
* A higher-order function which wraps an authProvider to handle refreshing authentication.
* This is useful when the authentication service supports a refresh token mechanism.
* The wrapped provider will call the refreshAuth function before
* calling the authProvider checkAuth, getIdentity and getPermissions methods.
*
* The refreshAuth function should return a Promise that resolves when the authentication token has been refreshed.
* It might throw an error if the refresh failed. In this case, react-admin will handle the error as usual.
*
* @param provider An authProvider
* @param refreshAuth A function that refreshes the authentication token if needed and returns a Promise.
* @returns A wrapped authProvider.
*
* @example
* import { addRefreshAuthToAuthProvider } from 'react-admin';
* import { authProvider } from './authProvider';
* import { refreshAuth } from './refreshAuth';
*
* const authProvider = addRefreshAuthToAuthProvider(authProvider, refreshAuth);
*/
export const addRefreshAuthToAuthProvider = (
provider: AuthProvider,
refreshAuth: () => Promise<void>
): AuthProvider => {
const proxy = new Proxy(provider, {
get(_, name) {
const shouldIntercept = AuthProviderInterceptedMethods.includes(
name.toString()
);

if (shouldIntercept) {
return async (...args: any[]) => {
await refreshAuth();
return provider[name.toString()](...args);
};
}

return provider[name.toString()];
},
});

return proxy;
};

const AuthProviderInterceptedMethods = [
'checkAuth',
'getIdentity',
'getPermissions',
];
36 changes: 36 additions & 0 deletions packages/ra-core/src/auth/addRefreshAuthToDataProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { DataProvider } from '../types';

/**
* A higher-order function which wraps a dataProvider to handle refreshing authentication.
* This is useful when the authentication service supports a refresh token mechanism.
* The wrapped provider will call the refreshAuth function before calling any dataProvider methods.
*
* The refreshAuth function should return a Promise that resolves when the authentication token has been refreshed.
* It might throw an error if the refresh failed. In this case, react-admin will handle the error as usual.
*
* @param provider A dataProvider
* @param refreshAuth A function that refreshes the authentication token if needed and returns a Promise.
* @returns A wrapped dataProvider.
*
* @example
* import { addRefreshAuthToDataProvider } from 'react-admin';
* import { jsonServerProvider } from 'ra-data-json-server';
* import { refreshAuth } from './refreshAuth';
*
* const dataProvider = addRefreshAuthToDataProvider(jsonServerProvider('http://localhost:3000'), refreshAuth);
*/
export const addRefreshAuthToDataProvider = (
provider: DataProvider,
refreshAuth: () => Promise<void>
): DataProvider => {
const proxy = new Proxy(provider, {
get(_, name) {
return async (...args: any[]) => {
await refreshAuth();
return provider[name.toString()](...args);
};
},
});

return proxy;
};
2 changes: 2 additions & 0 deletions packages/ra-core/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export * from './useAuthenticated';
export * from './useCheckAuth';
export * from './useGetIdentity';
export * from './useHandleAuthCallback';
export * from './addRefreshAuthToAuthProvider';
export * from './addRefreshAuthToDataProvider';

export {
AuthContext,
Expand Down