Authentication with React Router and Spring Security - reactjs

I am building a full stack application with React and Spring Boot, but came across one problem that I didn't find an answer anywhere.
The behavior that I am trying to achieve is as follows:
If a user wants to visit login or register page, I want to redirect user to the home page if they are authenticated. The isAuthenticated is a flag in redux that I set when the app first loads.
I also want to redirect user to the login page if they are not authenticated and tries to visit home page.
The first goal works fine as there are plenty of tutorials out there explaining how to use react-router to achieve that, but the first goal seems not to work properly in production.
I have a Spring Boot application running as a REST API on the backend, and I am using eirslett/frontend-maven-plugin to package both my front and backend into a single jar for deployment.
When I spin-up both front and backend separately as two development servers locally, the app will redirect me to the login page if I am not logged in (if I try to access secured routes by react-router). However, it will give me an error 401 page if I went directly to the /login route when not authenticated, but it will not give me error if I try to visit the home route and gets redirected to /login. Therefore, even if I got redirected to the /login just fine, I can't refresh the page or it will give me error 401.
The root component of my React front end looks like this, which utilizes React-router:
function App() {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<div className='AppContainer'>
<Navbar />
<Switch>
<PublicRoute path='/login' component={Login} />
<PublicRoute path='/register' component={Register} />
<PrivateRoute path='/' component={Todos} />
</Switch>
</div>
</ConnectedRouter>
</Provider>
);
}
export default App;
The PublicRoute and PrivateRoute component that I used looks as follow:
function PrivateRoute({ component: Component, isAuthenticated, ...rest }) {
return (
<Route
{...rest}
render={props =>
isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: '/login', state: { from: props.location } }}
/>
)
}
></Route>
);
}
const mapStateToProps = state => ({
isAuthenticated: state.auth.isAuthenticated
});
export default connect(mapStateToProps)(PrivateRoute);
function PublicRoute({ component: Component, isAuthenticated, ...rest }) {
return (
<Route
{...rest}
render={props =>
isAuthenticated ? (
<Redirect to={{ pathname: '/', state: { from: props.location } }} />
) : (
<Component {...props} />
)
}
></Route>
);
}
const mapStateToProps = state => ({
isAuthenticated: state.auth.isAuthenticated
});
export default connect(mapStateToProps)(PublicRoute);
Does anyone have any idea what is going on? I feel like the problem exists because of the way I configured my PublicRoute component and Spring Security, but I don't know how to make them work together. Or, to my naive understanding, my frontend application will only load if I first visit my root route therefore it will redirect me to /login just fine; but if the first page I visit is /login, the root component of React will not load and therefore this route is secured by my Spring Security configuration. But is there a way to get around with that?
Any help is appreciated!

I figured it out by realizing that I am using BrowserRouter instead of a HashRouter, and that is why I am getting 401 and 404. The react-router documentation actually provides a great explanation on why should use a HashRouter here.

Related

On React Router, stay on the same page even if refreshed

my site is built using MERN stack, when I refresh a page it first shows the main page and then the page where the user is. How to fix this issue?
For example:
if I refresh (/profile) page then for a meanwhile it shows (/) then it redirects to (/profile). I want if I refresh (/profile) it should be on the same page.
import { Route, Redirect } from 'react-router-dom';
const PrivateRoute = ({ component: Component, authed, ...rest }) => {
return (
<Route
{...rest}
render={(props) => authed === true
? <Component {...props} />
: <Redirect to={{ pathname: '/', state: { from: props.location } }} />}
/>
)
}
export default PrivateRoute;
Router code:
const App = () => {
const user = useSelector((state) => state?.auth);
return (
<>
<BrowserRouter>
<Container maxWidth="lg">
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" exact component={About} />
<Route path="/terms" exact component={Terms} />
<PrivateRoute authed={user?.authenticated} path='/profile' component={Profile} />
</Switch>
</Container>
</BrowserRouter>
</>
)
}
export default App;
How to fix so that user stays on the same page if its refreshed? The issue is on the pages where authentication is required.
When first authenticated the user, store the credentials(the info that you evaluate to see if the user is authenticated. Tokens etc.) in the localStorage. Of course you have to create necessary states too.
Then with useEffect hook on every render set the credential state from localStorage.
function YourComponentOrContext(){
const[credentials, setCredentials] = useState(null);
function yourLoginFunction(){
// Get credentials from backend response as response
setCredentials(response);
localStorage.setItem("credentials", response);
}
useEffect(() => {
let storedCredentials = localStorage.getItem("credentials");
if(!storedCredentials) return;
setCredentials(storedCredentials);
});
}
I guess on mounting (=first render) your user variable is empty. Then something asynchronous happen and you receive a new value for it, which leads to new evaluation of {user?.authenticated} resulting in true and causing a redirect to your /profile page.
I must say I'm not familiar with Redux (I see useSelector in your code, so I assume you are using a Redux store), but if you want to avoid such behaviour you need to retrieve the right user value on mounting OR only render route components when you've got it later.

Protecting routes in React app with React Router

I've created a simple React app with Redux, React Router and Auth0 which handles user authentications.
I'm trying to create this basic behavior to control access:
All unauthenticated users will automatically be sent to /public
Authenticated users can access all the other parts of the app
Once a user is authenticated by Auth0, I want to process the access_token and send user to / which is the Home component
Everything is "almost" working the way it should. The problem I'm having is that render() function in App.jsx is executing BEFORE the lock.on('authenticated') listener even has a chance to process the tokens returned by Auth0. As a result, the tokens are never stored and the user always seems to be unauthenticated. If I send user to /login, everything works fine because I'm not checking to see if the user is authenticated before rendering the Login component.
I think the way I'm handling protected routes needs to change. Any suggestions as to how to handle protected routes?
I'm providing the code that you need here. If you want to see the whole app, go to https://github.com/imsam67/react-redux-react-router-auth0-lock
The following is the App.jsx:
class App extends Component {
render() {
const isAuthed = isAuthenticated();
return (
<div>
<Switch>
<Route exact path="/" render={ props => isAuthed ? <Home {...props} /> : <Redirect to="/public" /> } />
<Route exact path="/login">
<Login />
</Route>
<Route path="/public">
<Public />
</Route>
</Switch>
</div>
);
}
}
This is the AuthWrapper component where I handle Auth0:
class AuthWrapper extends Component {
constructor(props) {
super(props);
this.onAuthenticated = this.onAuthenticated.bind(this);
this.lock = new Auth0Lock('my_auth0_client_id', 'my_domain.auth0.com', {
auth: {
audience: 'https://my_backend_api_url',
redirectUrl: 'http://localhost:3000/',
responseType: 'token id_token',
sso: false
}
});
this.onAuthenticated();
}
onAuthenticated() {
debugger; // After successful login, I hit this debugger
this.lock.on('authenticated', (authResult) => {
debugger; // But I never hit this debugger
let expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());
sessionStorage.setItem('access_token', authResult.accessToken);
sessionStorage.setItem('id_token', authResult.idToken);
sessionStorage.setItem('expires_at', expiresAt);
});
}
render() {
return(
<AuthContext.Provider value={{ lock: this.lock }}>
{this.props.children}
</AuthContext.Provider>
);
}
}
And here's index.js in case you need to see it:
import App from './components/App';
import AuthWrapper from './components/auth/AuthWrapper';
// Store
import appStore from './store/app-store';
const store = appStore();
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<AuthWrapper>
<App />
</AuthWrapper>
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
Here are the changes I made to make this work.
I now initialize Auth0 Lock in index.js to make it global
I moved the onAuthenticated() listener to LoginCallback.jsx component which is where the user is sent after a successful login. I believe moving the onAuthenticated() from App.jsx to LoginCallback.jsx made the biggest impact because the listener in LoginCallback.jsx executes before App.jsx.
On successful authentication, I also use this.props.history.push('/'); to send user to Home component
For full code, please see the repo at https://github.com/imsam67/react-redux-react-router-auth0-lock
Remember nothing is protected in the client side at all. If your concerned of routing to a component without auth just make sure no data is exposed(assuming they can't get any data without a token) and redirect if they landed there even after your router checks for auth or shows an error.
Remember nothing is protected in the client side at all. If your concerned of routing to a component without auth just make sure no data is exposed and redirect if they landed there even after your router checks for auth. I think #Sam has it right. The routes may not respond as expected to an asynchronous call changing it or may have odd behavior. I've never attempted a dynamic route this way but always had conditional renders of content components. A better approach may be to send the call and in the then block redirect to a url which the router knows to handle. Just not sure the router handles this very well. Catch the component checking for auth on load and redirect back to log on if not authorized. Sorry I'm not helping much here but conditional routes almost seem like an anti pattern but I guess it could work if we knew how the router renders its data after changes or if it actually does at all(the routes them selves.) So if they were to bookmark the url and try to return back that would be a 404 right? Maybe like a 401 unauthorized showing and redirect or link to log in might be better?
Dynamic routing need to be defined outside of the <Switch> scope. Here is an exemple assuming your function isAuthenticated() is a state (Redux or wathever)
import React from "react";
import ReactDOM from "react-dom";
import { createBrowserHistory } from "history";
import { Router, Route, Switch, Redirect } from "react-router-dom";
// core components
import Admin from "layouts/Admin.js";
import SignIn from "layouts/SignIn";
const hist = createBrowserHistory();
const loggedRoutes = () => (
<Switch>
<Route path="/" component={SignIn} />
<Route path="/admin" component={Admin} />
<Redirect from="/admin" to="/admin/aboutUs/whatWeDo" />
</Switch>
);
const routes = () => (
<Switch>
<Route path="/" component={SignIn} />
<Route path="/login" component={Admin} />
<Redirect from="/" to="/login" />
</Switch>
);
ReactDOM.render(
<Router history={hist}>
{checkIfAuth? loggedRoutes():routes()}
</Router>,
document.getElementById("root")
);
In this exemple, If you are not login you are redirect to /login.

React Routes redirect to external URL for Auth links

I have an example of the code where I am trying to redirect users which not log in yet. I am using external auth in order to send the user to login via 3rd party auth system. I know that react-routes have Redirect option, and I understand that they redirect only to pathnames. Is there a way to make it so that redirect happened with window.assign which redirect users right away to a different page?
Thank you in advance!
const ProtectedRoute = (auth, component: Component, ...rest) => {
return <Route
{...rest}
render={props => auth.isAuthenticated()
? <Component {...props} />
: <Redirect
to={{
pathname: 'http://example.com/',
state: { from: props.location },
}}
/>
}
/>;
};
You could evaluate the logging props in your component's componentDidMount hook and use window.location to redirect to entirely different URL. Eg:
componentDidMount() {
if(auth.isAuthenticated()) {
return <Component {...props} />
}
window.location.assign('http://example.com/');
}

React Router v4 - Redirect to home on page reload inside application

I need to redirect to home page when user refreshes other pages inside my application. I am using React router v4 and redux. Since the store is lost on reload, the page user reloaded is now empty and hence I want to take him back to a page that does not need any previous stored data. I don't want to retain state in localStorage.
I tried to handle this in event onload but it did not work:
window.onload = function() {
window.location.path = '/aaaa/' + getCurrentConfig();
};
You can try creating a new route component, say RefreshRoute and check for any state data you need. If the data is available then render the component else redirect to home route.
import React from "react";
import { connect } from "react-redux";
import { Route, Redirect } from "react-router-dom";
const RefreshRoute = ({ component: Component, isDataAvailable, ...rest }) => (
<Route
{...rest}
render={props =>
isDataAvailable ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/home"
}}
/>
)
}
/>
);
const mapStateToProps = state => ({
isDataAvailable: state.reducer.isDataAvailable
});
export default connect(mapStateToProps)(RefreshRoute);
Now use this RefreshRoute in your BrowserRouter as like normal Route.
<BrowserRouter>
<Switch>
<Route exact path="/home" component={Home} />
<RefreshRoute exact path="dashboard" component={Dashboard} />
<RefreshRoute exact path="/profile" component={ProfileComponent} />
</Switch>
</BrowserRouter>
It is so amazing that you don't want to keep state of user route map in browser but you use react-router!, the main solution for your case is do not use react-router.
If you don't use it, after each refresh the app come back to main view of app, If you wanna see route map in address bar without any reaction use JavaScript history pushState.
Hope it helps you.

React Router is redirecting to Login page but not rendering Login component

I am doing a very basic authentication workflow by using React Router v4 as follows:
const AuthenticatedRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
sessionStorage.getItem('jwt') ? (
<Component {...props} />
) : (
<Redirect to={{
pathname: "/login",
state: { from: props.location }
}} />
)
)}/>
)
ReactDOM.render(
<Provider store={createStoreWithMiddleware(reducers)}>
<BrowserRouter>
<div>
<AuthenticatedRoute exact path="/" component={App} />
<Route path="/login" component={LoginPage} />
</div>
</BrowserRouter>
</Provider>
, document.querySelector('.container'));
The flow works perfectly and when there is no 'jwt' the application is redirected to /login. However, strangely enough it is not getting rendered (i.e. blank page) until I press refresh from the browser.
Not sure how to debug and/or what could possible be the issue as this is a similar approach to most examples found online.
I'm guessing you use react-redux and it's a common issue. (If not I will delete answer)
use {pure: false} in react-redux connect or use withRouter HOC.
React router 4 does not update view on link, but does on refresh
I came here with same issue. My flow involved a modal popup in the product details page, clicking on OK button will do some task and take the user back to product listing page. Tried {pure: false}, and withConnect(connect.... Tried couple of other things as well. Nothing worked. Then the simplest thing worked. I opened the modal with this.setState({showConfirmationModal: true}); So in the successHandler, in the .then of the axios.get, I closed it with this.setState({showConfirmationModal: false}); and then used state property to render the <Redirect in render(). It worked !!. Hope this helps.

Resources