eazy-auth: Redux-based Authentication for React
eazy-auth is a JavaScript library designed to streamline common authentication tasks within React applications that leverage Redux and Redux-Saga. Currently at version 0.7.1, it provides a 'battery-included' solution for token-based authentication, handling login, token refresh, logout, and user data management. It integrates a Redux reducer, Redux-Saga flows for side effects (like API calls and token refreshing), and React components/HOCs for UI integration, alongside `react-router-dom` for protecting routes. The package allows developers to define custom API calls for authentication, making it adaptable to various backend systems while abstracting away much of the boilerplate. Its pre-1.0.0 status suggests ongoing development, though the README presents it as a functional solution. There is also `use-eazy-auth` which provides similar functionality using React hooks without the strong Redux/Redux-Saga dependency.
Common errors
-
Error: Invalid credentials (or similar network error during login)
cause The `loginCall` provided to `makeAuthFlow` rejected its promise due to incorrect credentials or a network issue with your authentication API.fixVerify the `loginCall` function logic, ensuring it correctly handles credentials and communicates with your backend. Check network requests in the browser developer tools for API errors. Ensure the backend returns the `access_token` and `refresh_token` in the expected format. -
TypeError: Cannot read properties of undefined (reading 'access_token') or similar when makeAuthFlow is called
cause The `loginCall` or `refreshTokenCall` functions did not return an object with `access_token` and `refresh_token` keys as expected by `eazy-auth`.fixReview your `loginCall` and `refreshTokenCall` implementations. Ensure they return a Promise that resolves to an object like `{ access_token: 'your_token', refresh_token: 'your_refresh_token' }`. -
React Router caught an unhandled error: You used a <Route> outside of a <Routes> context. Or 'AuthRoute' is not a <Route> component.
cause The `AuthRoute` component is being used with `react-router-dom` v6 or later, which has a different API compared to v4.x, or it's not wrapped within a `<Routes>` (v6) or `<Switch>` (v4/5) component.fixIf using `react-router-dom` v4 or v5, ensure `AuthRoute` is nested inside `<Switch>`. If using `react-router-dom` v6, the `AuthRoute` component from `eazy-auth` is incompatible. You will need to create a custom protected route component or use the `use-eazy-auth` library which is hooks-based.
Warnings
- breaking The package is currently in version 0.7.1, indicating a pre-1.0.0 status. This means that API changes, including breaking changes, might occur in minor or patch releases without following strict semantic versioning, as often happens before a stable major release. Developers should review release notes carefully for updates.
- gotcha `eazy-auth` stores access and refresh tokens in `localStorage` by default. While convenient for persistence across sessions, storing tokens in `localStorage` makes them vulnerable to Cross-Site Scripting (XSS) attacks. If an attacker can inject malicious JavaScript, they can steal these tokens, leading to session hijacking.
- gotcha The `AuthRoute` component provided by `eazy-auth` is designed for `react-router-dom` v4.1.x. It is not directly compatible with `react-router-dom` v6 or newer versions, which introduced significant API changes to route definition and navigation.
- gotcha The `makeAuthFlow` function requires `loginCall`, `refreshTokenCall`, and `meCall` to be asynchronous functions (returning Promises) that resolve to specific data structures (e.g., `{ access_token, refresh_token }` for login/refresh, and user data object for `meCall`). Incorrect return formats or promise rejections can lead to unexpected authentication flow failures or errors in the Redux state.
Install
-
npm install eazy-auth -
yarn add eazy-auth -
pnpm add eazy-auth
Imports
- makeAuthReducer
const makeAuthReducer = require('eazy-auth').makeAuthReducer;import { makeAuthReducer } from 'eazy-auth' - makeAuthFlow
const { authFlow, authCall } = require('eazy-auth').makeAuthFlow;import { makeAuthFlow } from 'eazy-auth' - login
import login from 'eazy-auth/actions/login';
import { login } from 'eazy-auth' - getAuthUser
import { selectors } from 'eazy-auth'; selectors.getAuthUser;import { getAuthUser } from 'eazy-auth' - AuthRoute
import AuthRoute from 'eazy-auth/components/AuthRoute';
import { AuthRoute } from 'eazy-auth'
Quickstart
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import createSagaMiddleware from 'redux-saga';
import { fork, call, put } from 'redux-saga/effects';
import { BrowserRouter as Router, Switch } from 'react-router-dom';
import { makeAuthReducer, makeAuthFlow, AuthRoute, login, getAuthUser } from 'eazy-auth';
// 1. Redux Reducer Setup
const rootReducer = combineReducers({
auth: makeAuthReducer(),
// ... other reducers
});
// 2. Redux Saga Setup
const loginMockCall = async (credentials) => {
console.log('Attempting login with:', credentials);
if (credentials.username === 'test' && credentials.password === 'password') {
return { access_token: 'fake-access-token', refresh_token: 'fake-refresh-token' };
}
throw new Error('Invalid credentials');
};
const refreshTokenMockCall = async (refreshToken) => {
console.log('Attempting token refresh with:', refreshToken);
if (refreshToken === 'fake-refresh-token') {
return { access_token: 'new-fake-access-token', refresh_token: 'new-fake-refresh-token' };
}
throw new Error('Invalid refresh token');
};
const meMockCall = async (token) => {
console.log('Fetching user data with token:', token);
if (token && token.startsWith('fake-')) {
return { id: 'user-123', username: 'testuser', email: 'test@example.com' };
}
throw new Error('Unauthorized');
};
const { authFlow, authCall } = makeAuthFlow({
loginCall: loginMockCall,
refreshTokenCall: refreshTokenMockCall,
meCall: meMockCall,
});
function* mainSaga() {
yield fork(authFlow);
// Example of using authCall for an authenticated API call
try {
const userData = yield authCall(async (token) => {
console.log('Authenticated API call with token:', token);
// Simulate an API call
return new Promise(resolve => setTimeout(() => resolve({ message: `Data for ${token}` }), 100));
});
yield put({ type: 'API_CALL_SUCCESS', payload: userData });
} catch (error) {
yield put({ type: 'API_CALL_FAILURE', error: error.message });
}
}
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(mainSaga);
// 3. React UI Setup
import { connect } from 'react-redux';
const LoginPage = ({ login, user }) => {
const handleSubmit = (e) => {
e.preventDefault();
login({ username: 'test', password: 'password' });
};
return (
<div>
<h2>Login</h2>
{user ? (
<p>Logged in as: {user.username}</p>
) : (
<form onSubmit={handleSubmit}>
<button type="submit">Log In (test/password)</button>
</form>
)}
</div>
);
};
const ConnectedLoginPage = connect(
(state) => ({ user: getAuthUser(state) }),
{ login }
)(LoginPage);
const ProfilePage = ({ user }) => (
<div>
<h2>Profile</h2>
{user ? (
<p>Welcome, {user.username}!</p>
) : (
<p>Please log in to view your profile.</p>
)}
</div>
);
const App = () => (
<Provider store={store}>
<Router>
<ConnectedLoginPage />
<Switch>
<AuthRoute path="/profile" component={ProfilePage} exact />
{/* Other routes */}
</Switch>
</Router>
</Provider>
);
ReactDOM.render(<App />, document.getElementById('root'));