This is 42's authentication module for Redux in combination with a specific Spring Security settings.
It can do the following things:
- Log the user in and out with your Spring application.
- Saving the current user in the Redux store.
- Send 'fetch' request with XSRF tokens and the cookies.
- Make a route only available for logged in users.
- Make a route available for specific users, based on properties of the current user.
We assume you have a working Redux project, if you do not yet have Redux add Redux to your project by following the Redux's instructions.
First install the following dependencies in the package.json:
- "react-redux": "5.0.3",
- "redux": "3.6.0",
- "react-router-dom": "4.0.0"
Now add the authentication-reducer to your rootReducer for example:
import { combineReducers } from 'redux';
import { authentication, AuthenticationStore } from 'redux-mad-authentication';
export interface Store {
authentication: AuthenticationStore
};
// Use ES6 object literal shorthand syntax to define the object shape
const rootReducer: Store = combineReducers({
authentication,
});
export default rootReducer;
This should add the AuthenticationStore to Redux, which will store the logged in user.
Next you have to configure the authentication module:
import { createStore } from 'redux';
import { configureAuthentication } from 'redux-mad-authentication';
export const store = createStore(
rootReducer,
);
configureAuthentication({
// The URL of your Spring back-end where the user can login (POST) and logout(DELETE)
authenticationUrl: '/api/authentication',
// The URL of your Spring back-end where the current user can be requested via GET
currentUserUrl: '/api/authentication/current',
// The route (in the front-end) the user should be redirected to when not logged in.
loginRoute: '/login',
// A reference to the dispatch function for the react store.
dispatch: store.dispatch,
// A function which returns the current 'authentication' store
authenticationStore: () => store.getState().authentication
});
The authentication module must be configured before the application is rendered.
In order to log the user in you must have a login form of some sorts. This library does not assume anything on how this login form should work or what it looks like.
Here's what a LoginForm should do:
- Call 'login' when the user submits the login form, with the correct body.
- Try to auto-login the user via
current
in the componentDidMount. - In the
render
Redirect the user when he is logged in.
For example:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Redirect, locationShape } from 'react-router-dom';
import { login, current } from 'redux-mad-authentication';
import { Store } from '../redux/root-reducer';
interface Props {
loggedIn: boolean,
location: locationShape
};
interface State {
username: string,
password: string,
error: boolean,
autoLoginFailed: boolean
};
export class Login extends Component<Props, State> {
state = {
username: '',
password: '',
error: false,
autoLoginFailed: false
};
// Calling `current()` automatically logs the user in when the session is still valid
componentDidMount() {
current().catch(() => {
this.setState({ autoLoginFailed: true });
});
}
onSubmit(event: Event) {
event.preventDefault();
const { username, password } = this.state;
this.setState({ error: false });
// `login` expects a body to send to the server
login({ username, password }).catch((error) => {
this.setState({ error: true });
});
}
setUsername(username: string) {
this.setState({ username });
}
setPassword(password: string) {
this.setState({ password });
}
render() {
// Be sure
if (this.props.loggedIn) {
const { from } = this.props.location.state || { from: { pathname: '/' } };
return <Redirect to={ from }/>;
}
if (this.state.autoLoginFailed === false) {
return null;
}
const { username, password, error } = this.state;
const errorMessage = error ? 'Username and password are incorrect' : '';
return (
<form>
<h1>Please log in</h1>
<p>
<label htmlFor="username">Username</label>
<input
id="username"
name="username"
type="text"
value={ username }
onChange={ (event) => this.setUsername(event.target.value) }
/>
</p>
<p>
<label htmlFor="password">Password</label>
<input
id="password"
name="password"
type="password"
value={ password }
onChange={ (event) => this.setPassword(event.target.value) }
/>
</p>
<p>{ errorMessage }</p>
<button type="sumbit" onClick={ (event) => this.onSubmit(event) }>Log in</button>
</form>
);
}
}
export default connect((store: Store) => {
return {
loggedIn: store.authentication.isLoggedIn
};
})(Login);
In order to logout you must call the 'logout' function, and make sure you Redirect the user to the login form when the user is logged in.
For example:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { Store } from '.redux/root-reducer';
import { logout } from 'redux-mad-authentication';
interface Props {
isLoggedIn: boolean
};
export class Logout extends Component<Props, void> {
onLogoutClick() {
logout();
}
render() {
if (this.props.isLoggedIn === false) {
return <Redirect to="/"/>;
}
return (
<a onClick={ () => this.onLogoutClick() }>Uitloggen</a>
);
}
}
export default connect((store: Store) => {
return {
isLoggedIn: store.authentication.isLoggedIn
};
})(Logout);
Some routes can only be accessible when the user is logged in. You can do this via the PrivateRoute for example:
<BrowserRouter history={ browserHistory }>
<div>
<Route exact path="/" component={ Dashboard } />
<Route path="/login" component={ Login }/>
<PrivateRoute path="/users" component={ Users } />
</div>
</BrowserRouter>
PrivateRoute works exacly like Route, except that it does not
support a render
method. You must always provide a Component
instead.
When the user tries to go to a PrivateRoute he will be redirected
to the Config's route loginRoute
.
Some routes can only be accessed by a type of user or a specific user. You can do this via the AuthorizedRoute.
<BrowserRouter>
<div>
<Route exact path="/" component={ Dashboard } />
<Route path="/login" component={ Login }/>
<PrivateRoute path="/users" component={ Users } />
<AuthorizedRoute
path="/pictures"
component={ Pictures }
authorizer={ (authenticationStore: AuthenticationStore) => {
return authenticationStore.currentUser.role === 'ADMIN';
}}
/>
</div>
</BrowserRouter>
The authorizer function is only ran if the user is logged in. The authorizer is given the authenticationStore as the first parameter, and is expected to return a boolean.
AuthorizedRoute works exacly like Route, except that it does not
support a render
method. You must always provide a Component
instead.
When the user tries to go to a AuthorizedRoute he will be redirected
to the Config's route loginRoute
.
Lets say you have a component which must render the current user's name, you will then need the current user from the Redux store.
We can use react-redux's connect to achieve this.
For example:
import { connect } from 'react-redux';
import { Store } from '.redux/root-reducer';
function User(props) {
if (isLoggedIn) {
return <h1>Hi {{ props.currentUser.name }}</h1>
} else {
return <h1>Please log in</h1>
}
}
export default connect((store: Store) => {
return {
isLoggedIn: store.authentication.isLoggedIn,
currentUser: store.authentication.currentUser
};
})(User);
To perform request with the XSRF token and with the cookies from the current user. You can use the 'authFetch' utility from this library:
import 'authFetch' from './authentication'
function getUser() {
authFetch('/user/1').then((response) => {
console.log(response);
});
}
authFetch
is a thin wrapper around fetch
, it only adds the
credentials and XSRF token, so it has the exact same arguments
as fetch
.