redux-saga
An OAuth module for Redux Saga powered applications
What does it do?
Redux Saga OAuth provides a reducer and a saga to handle authentication within any JavaScript application that uses Redux and Redux Saga.
Key features
- Has Flow support for easier integration
- Handles all HTTP requests with axios
- Allows for any grant types and extra data to be passed during login
- Automatically handles token refresh following the standard flow
- Handles failures during the refresh flow and will retry until it succeeds
- Allows the refresh token to be expired on the server and log users out
Getting started
Install
You can install this via yarn
or npm
, however, yarn
is the preferred
method.
yarn add @simpleweb/redux-saga-oauth
npm install --save @simpleweb/redux-saga-oauth
It also has a peer dependency of redux-saga
, please make sure this is
installed before hand.
Usage
Add the provided reducer to your store
Within your existing Redux store, bring in the provided reducer. It’s key
(auth
in the example below) can be customised to anything you like.
import { Reducer } from "@simpleweb/redux-saga-oauth";
const store = createStore(
combineReducers({
auth: Reducer,
})
);
Add the provided saga to your root saga
Create the provided auth saga and add it to your root saga. These are the
required options you must pass. The reducerKey
should match the key from
the step above.
import { createOAuthSaga } from "@simpleweb/redux-saga-oauth";
const authSaga = createOAuthSaga({
reducerKey: "auth",
OAUTH_URL: "http://localhost:3000/oauth/token.json",
OAUTH_CLIENT_ID: "<CLIENT ID>",
OAUTH_CLIENT_SECRET: "<CLIENT SECRET>",
});
const sagas = function* rootSaga() {
yield all([
fork(authSaga),
]);
}
Login and logout
To login, simply import the provided actions, pass through your API’s corresponding credentials and dispatch the action.
import { login, logout } from "@simpleweb/redux-saga-oauth";
const params = {
username: "ben@simpleweb.co.uk",
password: "mysecurepassword",
grant_type: "password",
};
store.dispatch(
login(params)
);
store.dispatch(
logout()
);
Actions
The module does expose all it’s internal Redux actions and constants should you need them. They are exposed like so.
import { Actions } from "@simpleweb/redux-saga-oauth";
Actions.authLoginRequest()
Actions.AUTH_LOGIN_REQUEST
Authenticated requests
This will something you will want to do after having got this working, while the code is not directly provided in the module, it's worth moving this into your own codebase as some sort of helper function to make authenticated requests using the access token in the store.
Please note any import
’s are missing from the code below.
App/Sagas/AuthenticatedRequest.js
// Custom error type to be thrown from this saga
// e.g. throw new AuthenticationSagaError('Some message');
function AuthenticationSagaError(message) {
this.message = message;
this.name = "AuthenticationSagaError";
}
// Helper function to get the authentication state from the store
// the "authentication" key will be unique to your code
const getAuthentication = state => state.auth;
// Helper function to check if the token has expired
export const tokenHasExpired = ({ expiresIn, createdAt }) => {
const MILLISECONDS_IN_MINUTE = 1000 * 60;
// Set refreshBuffer to 10 minutes
// so the token is refreshed before expiry
const refreshBuffer = MILLISECONDS_IN_MINUTE * 10;
// Expiry time
// multiplied by 1000 as server time are return in seconds, not milliseconds
const expiresAt = new Date((createdAt + expiresIn) * 1000).getTime();
// The current time
const now = new Date().getTime();
// When we want the token to be refreshed
const refreshAt = expiresAt - refreshBuffer;
return now >= refreshAt;
};
// Helper function to get the access token from the store
// if the token has expired, it will wait until the token has been refreshed
// or an authentication invalid error is thrown
function* getAccessToken() {
const authentication = yield select(getAuthentication);
// Expires_in, created_at
// If the token has expired, wait for the refresh action
if (
tokenHasExpired({
expiresIn: authentication.expires_in,
createdAt: authentication.created_at,
})
) {
yield race({
refreshError: take(AUTH_INVALID_ERROR),
tokenRefreshed: take(AUTH_REFRESH_SUCCESS),
});
}
// Return the latest access token
const latestAuthentication = yield select(getAuthentication);
return latestAuthentication.access_token;
}
// Finally the function you’ll use inside your sagas to make requests
export default function* AuthenticatedRequest(...args) {
// Get the current access token, wait for it if it needs refreshing
const accessToken = yield getAccessToken();
if (accessToken) {
const config = {
headers: {
Authorization: `Bearer ${accessToken}`,
},
};
try {
return yield call(...args, config);
} catch (error) {
if (error.response && error.response.status === 401) {
yield put(authInvalidError(error.response));
throw new AuthenticationSagaError("Unauthorized");
} else {
throw error;
}
}
} else {
throw new AuthenticationSagaError("No access token");
}
}
Usage
The AuthenticatedRequest
function simply wraps your normally API calls so
additional headers can be passed down to add in the access token.
import axios from "axios";
import AuthenticatedRequest from "App/Sagas/AuthenticatedRequest";
function* MakeRequest() {
try {
const response = yield AuthenticatedRequest(axios.get, "/user");
} catch(error) {
}
}