Skip to content

Commit

Permalink
Moving all auth config to authClient
Browse files Browse the repository at this point in the history
  • Loading branch information
fzaninotto committed Jan 26, 2017
1 parent 3e632a0 commit afda4b2
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 141 deletions.
50 changes: 43 additions & 7 deletions docs/AdminResource.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ Here are all the props accepted by the component:
* [`appLayout`](#applayout)
* [`customReducers`](#customreducers)
* [`customSagas`](#customsagas)
* [`authentication`](#authentication)
* [`authClient`](#authclient)
* [`loginPage`](#loginpage)
* [`logoutButton`](#logoutbutton)
* [`locale`](#internationalization)
* [`messages`](#internationalization)

Expand Down Expand Up @@ -284,25 +286,59 @@ const App = () => (
export default App;
```

### `authentication`
### `authClient`

The `authentication` props expect an object with `authClient` and `checkCredentials` methods, to control the application authentication strategy:
The `authClient` prop expect a function returning a Promise, to control the application authentication strategy:

```js
const authentication = {
authClient(type, params) { /* ... */ },
checkCredentials(nextState, replace) { /* ... */},
import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_CHECK } from 'admin-on-rest';

const authClient(type, params) {
// type can be any of AUTH_LOGIN, AUTH_LOGOUT, AUTH_CHECK
// ...
return Promise.resolve();
};

const App = () => (
<Admin authentication={authentication} restClient={jsonServerRestClient('http://jsonplaceholder.typicode.com')}>
<Admin authClient={authClient} restClient={jsonServerRestClient('http://jsonplaceholder.typicode.com')}>
...
</Admin>
);
```

The [Authentication documentation](./Authentication.html) explains how to implement these functions in detail.

### `loginPage`

If you want to customize the Login page, or switch to another authentication strategy than a username/password form, pass a component of your own as the `loginPage` prop. Admin-on-rest will display this component whenever the `/login` route is called.

```js
import MyLoginPage from './MyLoginPage';

const App = () => (
<Admin loginPage={MyLoginPage}>
...
</Admin>
);
```

See The [Authentication documentation](./Authentication.html#customizing-the-login-and-logout-components) for more explanations.

### `logoutButton`

If you customize the `loginPage`, you probably need to override the `logoutButton`, too - because they share the authentication strategy.

```js
import MyLoginPage from './MyLoginPage';
import MyLogoutButton from './MyLogoutButton';

const App = () => (
<Admin loginPage={MyLoginPage} logoutButton={MyLogoutButton}>
...
</Admin>
);
```

### Internationalization

The `locale` and `messages` props let you translate the GUI. The [Translation Documentation](./Translation.html) details this process.
Expand Down
172 changes: 92 additions & 80 deletions docs/Authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,45 +11,29 @@ Admin-on-rest lets you secure your admin app with the authentication strategy of

By default, an admin-on-rest app doesn't require authentication. But if the REST API ever returns a 401 (Unauthorized) or a 403 (Forbidden) response, then the user is redirected to the `/login` route. You have nothing to do - it's already built in.

## Authentication Configuration

You can configure authentication in the `<Admin>` component, by passing an object as the `authentication` prop:

```js
const authentication = {
// structure described below
};

<Admin authentication={authentication}>
...
</Admin>
```

This `authentication` object allows to configure the login and logout HTTP calls, the credentials check made during navigation, and the Login and Logout components. Read on to see that in detail.

## Configuring the Auth Client

By default, the `/login` route renders a special component called `Login`, which displays a login form asking for username and password.

![Default Login Form](./img/login-form.png)

What this form does upon submission depends on the `authClient` method of the `authentication` object. This method receives authentication requests `(type, params)`, and should return a Promise. `Login` calls `authClient` with the `AUTH_LOGIN` type, and `{ login, password }` as parameters. It's the ideal place to authenticate the user, and store their credentials.
What this form does upon submission depends on the `authClient` prop of the `<Admin>` component. This function receives authentication requests `(type, params)`, and should return a Promise. `Login` calls `authClient` with the `AUTH_LOGIN` type, and `{ login, password }` as parameters. It's the ideal place to authenticate the user, and store their credentials.

For instance, to query an authentication route via HTTPS and store the response (a token) in local storage, configure `authClient` as follows:
For instance, to query an authentication route via HTTPS and store the credentials (a token) in local storage, configure `authClient` as follows:

```js
// in src/authClient.js
import { AUTH_LOGIN } from 'admin-on-rest';

const authentication = {
authClient(type, params) {
if (type === AUTH_LOGIN) {
const { username, password } = params;
const request = new Request('https://mydomain.com/authenticate', {
method: 'POST',
body: JSON.stringify({ username, password }),
headers: new Headers({ 'Content-Type': 'application/json' }),
})
return fetch(request)
export default (type, params) => {
if (type === AUTH_LOGIN) {
const { username, password } = params;
const request = new Request('https://mydomain.com/authenticate', {
method: 'POST',
body: JSON.stringify({ username, password }),
headers: new Headers({ 'Content-Type': 'application/json' }),
})
return fetch(request)
.then(response => {
if (response.status < 200 || response.status >= 300) {
throw new Error(response.statusText);
Expand All @@ -59,19 +43,31 @@ const authentication = {
.then(({ token }) => {
localStorage.setItem('token', token)
});
}
return Promise.resolve();
}
return Promise.resolve();
}
```

**Tip**: It's a good idea to store credentials in `localStorage`, to avoid reconnection when opening a new browser tab. But this makes your application [open to XSS attacks](http://www.redotheweb.com/2015/11/09/api-security.html), so you'd better double down on security, and add an `httpOnly` cookie on the server side, too.

When the `authClient` Promise resolves, the login form redirects to the previous page, or to the admin index if the user just arrived.
Then, pass this client to the `<Admin>` component:

```js
// in src/App.js
import authClient from './authClient';

const App = () => (
<Admin authClient={authClient}>
...
</Admin>
);
```

Upon receiving a 403 response, the admin app shows the Login page. `authClient` is now called when the user submits the login form. Once the promise resolves, the login form redirects to the previous page, or to the admin index if the user just arrived.

## Sending Credentials to the REST API

To use the credentials when calling REST API routes, you need to configure the `httpClient` passed as second parameter to `simpleRestClient` or `jsonServerRestClient`, as explained in the [REST client documentation](RestClients.html#adding-custom-headers).
To use the credentials when calling REST API routes, you have to tweak, this time, the `restClient`. As explained in the [REST client documentation](RestClients.html#adding-custom-headers), `simpleRestClient` and `jsonServerRestClient` take an `httpClient` as second parameter. That's the place where you can change request headers, cookies, etc.

For instance, to pass the token obtained during login as an `Authorization` header, configure the REST client as follows:

Expand All @@ -87,32 +83,34 @@ const httpClient = (url, options) => {
}
const restClient = simpleRestClient('http://localhost:3000', httpClient);

<Admin restClient={restClient}>
...
</Admin>
const App = () => (
<Admin restClient={restClient} authClient={authClient}>
...
</Admin>
);
```

If you have a custom REST client, don't forget to add credentials yourself.

## Adding a Logout Button

If you provide an `authClient` method to the `authentication` prop, admin-on-rest will display a logout button in the sidebar. When the user clicks on the logout button, this calls the `authClient` with the `AUTH_LOGOUT` type. When resolved, the user gets redirected to the login page.
If you provide an `authClient` prop to `<Admin>`, admin-on-rest displays a logout button in the sidebar. When the user clicks on the logout button, this calls the `authClient` with the `AUTH_LOGOUT` type. When resolved, the user gets redirected to the login page.

For instance, to remove the token from local storage upon logout:

```js
// in src/authClient.js
import { AUTH_LOGIN, AUTH_LOGOUT } from 'admin-on-rest';

const authentication = {
authClient(type, params) {
if (type === AUTH_LOGIN) {
// ...
}
if (type === AUTH_LOGOUT) {
localStorage.removeItem('token');
}
export default (type, params) => {
if (type === AUTH_LOGIN) {
// ...
}
if (type === AUTH_LOGOUT) {
localStorage.removeItem('token');
return Promise.resolve();
}
return Promise.resolve();
};
```

Expand All @@ -124,65 +122,79 @@ The `authClient` is also a good place to notify the authentication API that the

Admin-on-rest redirects to the login page whenever a REST response uses a 403 status code. But that's usually not enough, because admin-on-rest keeps data on the client side, and could display stale data while contacting the server - even after the credentials are no longer valid.

That means you need a way to check credentials during navigation. That's the purpose of the `checkCredentials` method of the `authentication` object. It's a kind of middleware function that is called by the router before every page change, so it's the ideal place to check for credentials.
Fortunately, each time the user navigates, admin-on-rest calls the `authClient` with the `AUTH_CHECK` type, so it's the ideal place to check for credentials.

For instance, to check for the existence of the token in local storage:

```js
const authentication = {
authClient(type, params) { /* ... */ },
checkCredentials(nextState, replace) {
if (!localStorage.getItem('token')) {
replace({
pathname: '/login',
state: { nextPathname: nextState.location.pathname },
});
}
},
}
// in src/authClient.js
import { AUTH_LOGIN, AUTH_LOGOUT } from 'admin-on-rest';

export default (type, params) => {
if (type === AUTH_LOGIN) {
// ...
}
if (type === AUTH_LOGOUT) {
// ...
}
if (type === AUTH_CHECK) {
return localStorage.getItem('username') ? Promise.resolve() : Promise.reject();
}
return Promise.reject('Unkown method');
};
```

**Tip**: The `replace` function is passed by the router ; it allows to redirect the user if the check fails. Passing `state.nextPathname` allows the `Login` form to redirect to the page required by the user after authentication.
If the promise is rejected, admin-on-rest redirects to the `/login` page.

**Tip**: You can override the `checkCredentials` in `<Resource>` components, to implement different checks for different resources:
**Tip**: For the `AUTH_CHECK` call, the `params` argument contains the `resource` name, so you can implement different checks for different resources:

```js
import { checkCredentialsForPowerUser, checkCredentials } from './credentials';
// in src/authClient.js
import { AUTH_LOGIN, AUTH_LOGOUT } from 'admin-on-rest';

const App = () => (
<Admin>
<Resource name="customers" checkCredentials={checkCredentials} list={CustomersList} />
<Resource name="bank_accounts" checkCredentials={checkCredentialsForPowerUser} list={AccountsList} />
</Admin>
);
export default (type, params) => {
if (type === AUTH_LOGIN) {
// ...
}
if (type === AUTH_LOGOUT) {
// ...
}
if (type === AUTH_CHECK) {
const { resource } = params;
if (resource === 'posts') {
// check credentials for the posts resource
}
if (resource === 'comments') {
// check credentials for the comments resource
}
}
return Promise.reject('Unkown method');
};
```

**Tip**: The `authClient` can only be called with `AUTH_LOGIN`, `AUTH_LOGOUT`, or `AUTH_CHECK`; that's why the final return is a rejected promise.

## Customizing The Login and Logout Components

Using `authClient` and `checkCredentials` is enough to implement a full-featured authorization system if the authentication relies on a username and password.

?B?u?t??w?h?a?t??i?f??y?o?u??w?a?n?t??t?o??u?s?e??a?n??e?m?a?i?l??i?n?s?t?e?a?d??o?f??a??u?s?e?r?n?a?m?e????W?h?a?t??i?f??y?o?u??w?a?n?t??t?o??u?s?e??a??S?i?n?g?l?e?-?S?i?g?n?-?O?n??(?S?S?O?)??w?i?t?h??a??t?h?i?r?d?-?p?a?r?t?y??a?u?t?h?e?n?t?i?c?a?t?i?o?n??s?e?r?v?i?c?e?????W?h?a?t??i?f??y?o?u??w?a?n?t??t?o??u?s?e??t?w?o?-?f?a?c?t?o?r??a?u?t?h?e?n?t?i?c?a?t?i?o?n???
But what if you want to use an email instead of a username? What if you want to use a Single-Sign-On (SSO) with a third-party authentication service? What if you want to use two-factor authentication?

For all these cases, it's up to you to implement your own `LoginPage` component, which will be displayed under the `/login` route instead of the default username/password form. You can customize the `LoginPage` component via the `authentication` object. You can do the same with the `LogoutButton` component:
For all these cases, it's up to you to implement your own `LoginPage` component, which will be displayed under the `/login` route instead of the default username/password form, and your own `LogoutButton` component, which will be displayed in the sidebar. Pass both these components to the `<Admin>` component:

```js
// in src/App.js
import MyLoginPage from './MyLoginPage';
import MyLogoutButton from './MyLogoutButton';

const authentication = {
LoginPage: MyLoginPage,
LogoutButton: MyLogoutButton,
checkCredentials(nextState, replace) {
if (!localStorage.getItem('token')) {
replace({
pathname: '/login',
state: { nextPathname: nextState.location.pathname },
});
}
},
}
const App = () => (
<Admin loginPage={MyLoginPage} logoutButton={MyLogoutButton}>
...
</Admin>
);
```

**Tip**: When customizing `LoginPage` and `LogoutButton`, you no longer need the `authClient` method, since it is only passed to the default `Login` and `Logout` components.

**Tip**: When setting the `loginPage` and `logoutButton` props, you no longer need the `authClient` method, since it is only passed to the default `Login` and `Logout` components. That means your custom login and logout components must implement their own authentication client.

**Tip**: If you want to use Redux and Saga to handle credentials and authorization, you will need to register [custom reducers](./AdminResource.html#customreducers) and [custom sagas](./AdminResource.html#customsagas) in the `<Admin>` component.
Loading

0 comments on commit afda4b2

Please sign in to comment.