I'm developing a full stack PERN application using React/Redux, Knex + Objection.Js + PostgreSQL for the DB, and feathersjs for the API framework. As such, I'm using @feathersjs/client
on the frontend as well and their authentication package. I'm also using connected-react-router
for my routing. Unfortunately, whenever I attempt to navigate to a protected route, the "login" request responsible for setting the state of the user (from their jwt authenticating with the server) doesn't finish before the redirect takes the user to the login page.
I'm validating the jwt in the index.js
file of the react application by dispatching an action.
if (localStorage['feathers-jwt']) {
try {
store.dispatch(authActions.login({strategy: 'jwt', accessToken: localStorage.getItem('feathers-jwt')}));
}
catch (err){
console.log('authenticate catch', err);
}
}
The action is picked up by redux-saga
which performs the following action
export function* authSubmit(action) {
console.log('received authSubmit');
try {
const data = yield call(loginApi, action);
yield put({type: authTypes.LOGIN_SUCCESS, data});
} catch (error) {
console.log(error);
yield put({type: authTypes.LOGIN_FAILURE, error})
}
}
function loginApi(authParams) {
return services.default.authenticate(authParams.payload)
}
Here's my isAuthenticated
function with configuration object:
const isAuthenticated = connectedReduxRedirect({
redirectPath: '/login',
authenticatedSelector: state => state.auth.user !== null,
redirectAction: routerActions.replace,
wrapperDisplayName: 'UserIsAuthenticated'
});
Here's the HOC being applied to the container components
const Login = LoginContainer;
const Counter = isAuthenticated(CounterContainer);
const LoginSuccess = isAuthenticated(LoginSuccessContainer);
And finally, here's the render
export default function (store, history) {
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<Switch>
<Route exact={true} path="/" component={App}/>
<Route path="/login" component={Login}/>
<Route path="/counter" component={Counter}/>
<Route path="/login-success" component={LoginSuccess}/>
<Route component={NotFound} />
</Switch>
</ConnectedRouter>
</Provider>,
document.getElementById('root')
);
}
What I expect to happen, when logged in and visiting, for example, /counter
is the following
LOGIN_REQUEST action fired
LOGIN_SUCCESS action fired, user is authenticated by JWT
router sees user.auth object isn't null, therefore the user is Authenticated
router permits navigation without redirection
What I see instead is the following (when navigating manually to /counter
)
@@INIT
auth/LOGIN_REQUEST [this is good, loggingIn: true
]
@@router/LOCATION_CHANGE
{
type: '@@router/LOCATION_CHANGE',
payload: {
location: {
pathname: '/counter',
search: '',
hash: ''
},
action: 'POP',
isFirstRendering: true
}
}
type: '@@router/LOCATION_CHANGE',
payload: {
location: {
pathname: '/login',
hash: '',
search: '?redirect=%2Fcounter',
key: 'kdnf4l'
},
action: 'REPLACE',
isFirstRendering: false
}
}
User navigates to /login
, which logs the user out as it's currently designed.
LOGOUT_REQUEST -> LOGIN_SUCCESS -> LOCATION_CHANGE (to /login-success
)
Again, any help would be greatly appreciated and I can provide anything else as is needed.
Thanks!
-Brenden
I was able to solve this today by taking a look at how the authentication package feathers-reduxify-authentication
function. The redirect was, for the most part, configured correctly.
authentication.js
Note multiple strategies, and how the context.result is returned. This is necessary for feathers-reduxify-authentication
to work properly.
module.exports = function (app) {
const config = app.get('authentication');
// Set up authentication with the secret
app.configure(authentication(config));
app.configure(jwt());
app.configure(local(config.local));
app.service('authentication').hooks({
before: {
create: [
authentication.hooks.authenticate(config.strategies),
],
remove: [
authentication.hooks.authenticate('jwt')
]
},
after: {
create: [
context => {
context.result.data = context.params.user;
context.result.token = context.data.accessToken;
delete context.result.data.password;
return context;
}
]
}
});
};
src/feathers/index.js
This is according to eddystop's example project, but upgraded to feathers 3.0+
import feathers from '@feathersjs/client';
import io from 'socket.io-client';
import reduxifyAuthentication from 'feathers-reduxify-authentication';
import reduxifyServices, { getServicesStatus } from 'feathers-redux';
import { mapServicePathsToNames, prioritizedListServices } from './feathersServices';
const hooks = require('@feathersjs/client');
const socket = io('http://localhost:3030');
const app = feathers()
.configure(feathers.socketio(socket))
.configure(hooks)
.configure(feathers.authentication({
storage: window.localStorage
}));
export default app;
// Reduxify feathers-client.authentication
export const feathersAuthentication = reduxifyAuthentication(app,
{ authSelector: (state) => state.auth.user}
);
// Reduxify feathers services
export const feathersServices = reduxifyServices(app, mapServicePathsToNames);
export const getFeathersStatus =
(servicesRootState, names = prioritizedListServices) =>
getServicesStatus(servicesRootState, names);
middleware and store. src/state/configureStore
redux-saga is temporarily removed, I'll be bringing it back once I finish testing
import { createBrowserHistory } from 'history';
import { createStore, applyMiddleware, compose } from "redux";
import { routerMiddleware } from 'connected-react-router';
import createRootReducer from './ducks';
import promise from 'redux-promise-middleware';
import reduxMulti from 'redux-multi';
import rootSaga from '../sagas';
import createSagaMiddleware from 'redux-saga';
export default function configureStore(initialState) {
const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|| compose;
const middlewares = [
//sagaMiddleware,
promise,
reduxMulti,
routerMiddleware(history)];
const store = createStore(
createRootReducer(history),
initialState,
composeEnhancer(
applyMiddleware(
...middlewares
)
)
);
return store;
}
root reducers, src/state/ducks/index.js
import { combineReducers } from "redux";
import { connectRouter } from 'connected-react-router';
import { reducer as reduxFormReducer } from 'redux-form';
import {feathersAuthentication, feathersServices} from '../../feathers';
import counter from './counter';
const rootReducer = (history) => combineReducers({
counter,
router: connectRouter(history),
users: feathersServices.users.reducer,
auth: feathersAuthentication.reducer,
form: reduxFormReducer, // reducers required by redux-form
});
export default rootReducer;