Login in React.js with localStorage token - reactjs

Im developing a Login system of a React WebSite. We're using api_tokens for access to the API and retrieve the info as validate user credentials. I need to restringe everything when the auth fails and redirect to the user login page, currently Im using redux as app global state. The issue is I'm saving the api_token in browser localStorage, and I need to dispatch the UNAUTH_USER when the api_token is modified by Browser Clear History or other things........
I was wondering if I can attach some eventListener to that... and if is the right solution..
The code looks below:
import React from 'react'
import Header from './Header'
import Navigation from '../navigation/components/Navigation'
import Ribbon from '../ribbon/Ribbon'
import Footer from './Footer'
import Shortcut from '../navigation/components/Shortcut'
import LayoutSwitcher from '../layout/components/LayoutSwitcher'
import {connect} from 'react-redux'
// require('../../smartadmin/components/less/components.less');
class Layout extends React.Component {
constructor(props) {
super(props);
}
componentWillMount() {
if(!this.props.authenticated) {
this.props.router.push('/login');
}
}
componentWillUpdate(nextProps) {
if(!nextProps.authenticated) {
this.props.router.push('/login');
}
}
render() {
if (!this.props.authenticated) {
this.props.router.push('/login');
return null;
}
else {
return (
<div>
<Header />
<Navigation />
<div id="main" role="main">
<LayoutSwitcher />
<Ribbon />
{this.props.children}
</div>
<Footer />
<Shortcut />
</div>
)
}
}
}
const mapStateToProps = (state) => {
return {
authenticated: state.auth.authenticated
};
}
export default connect(mapStateToProps)(Layout);

I do the same thing in my app. For every call my Sagas make, if an api error occurs and a 403 expired/unvalid token is raised, I dispatch there a redux action that clears localstorage and disconnect the user (I have a refresh token mechanism retry before that).
I'm not sure that you can attach an eventListener to a localStorage expiration, but placing a check at your API calls mechanism would be IMO a good practice.
Best

Related

how to keep a set state after redirecting to outside website, then redirecting back to react app

I'm new to react and am building an app which can connect to one of two APIs. These API's each use the "authorization code" flow, which redirects the user to an identity server, the user logs into the identity server and allows my react app to access their data stored in a database, and the user is then redirected back to my app.
So my app has a home page with 2 buttons, button1 and button2. I'm using the home page state to store the value of which button was clicked and which one of the APIs the user wants to use - eg: if button1 was clicked: this.setState({page1: true, page2: false})
Then the user is redirected to the identity server: window.location = "https://identityserverurl"
The user logs in to the identity server website, clicks "Allow" and is then redirected back to my app. What should happen then is based on the state set just before the redirect to the identity server, a different component is displayed instead of the home page - (im using a conditional statement for that).
The problem is when the user is redirected back to my app from the APIs identity server, it loads the home page instead of one of the other pages because the state is being reset to its initial value when the identity server redirect the user back to my app.
I need to find a way to prevent the state from being reset back to the initial value when the identity server redirect back to my app. I'm not sure how to handle this.
Here is my code for reference.
Main.js
import React, { Component } from 'react';
import Home from './Home.js';
import Page1 from './Page1.js';
import Page2 from './Page2.js'
class Main extends Component {
constructor(props) {
super(props);
this.state = {page1: false, page2: false};
this.handleClick = this.handleClick.bind(this);
}
hanldeRedirect() {
window.location = "https://identityserverurl.com";
}
handleClick(arg) {
if(arg === "page1") {
this.setState({page1: true, page2: false});
this.hanldeRedirect();
}
if(arg === "page2") {
this.setState({page1: false, page2: true});
this.hanldeRedirect();
}
if(arg === "home"){
this.setState({page1: false, page2: false})
}
}
render() {
const isPage1 = this.state.page1;
const isPage2 = this.state.page2;
if(!isPage1 && !isPage2) {
return(<Home onPage1Event={() => this.handleClick("page1")} page1Value="page1" onPage2Event={() => this.handleClick("page2")} page2Value="page2" />)
}
else {
if(isPage1) {
return(<Page1 />)
}
else {
return(<Page2 />)
}
}
}
}
export default Main;
Home.js
import React, { Component } from 'react';
import Button from './Button.js';
class Home extends Component {
render() {
return(
<div>
<Button onEvent={(event)=> this.props.onPage1Event(event)} btnValue={this.props.page1Value}>{this.props.page1Value}</Button>
<br/>
<Button onEvent={(event)=> this.props.onPage2Event(event)} btnValue={this.props.page2Value}>{this.props.page2Value}</Button>
</div>
)
}
}
export default Home;
Page1.js
import React from 'react';
const Page1 = () => {
return(
<div>
<h1>Page1</h1>
</div>
)
}
export default Page1;
Page2.js
import React from 'react';
const Page2 = () => {
return(
<div>
<h1>Page2</h1>
</div>
)
}
export default Page2;
Button.js
import React, { Component } from 'react';
class Button extends Component {
render() {
return(
<button onClick={() => this.props.onEvent()} value={this.props.btnValue}>{this.props.btnValue}</button>
)
}
}
export default Button
Use browser local storage that can give a persistent storage, since your react app get reloaded each redirection,
Use the value stored in local as the state value.
For further info:
https://www.robinwieruch.de/local-storage-react

Is it possible to trigger function after logging in by withAuthenticator() of AWS Amplify?

I would like to trigger function when user login by withAuthenticator() of Amplify.
(I need to send state data to other component by contextAPI of React.js)
I've found explanation of SignIn() function, but I didn't find something about function when users login
▼Main.tsx (Main page, everyone can watch)
import React from 'react';
import { BrowserRouter as Router, Route } from "react-router-dom";
import Login from './Login';
import Mypage from './mypage/Index';
import Menu from './Menu';
import Hoge from './Hoge';
class Main extends React.Component {
render(){
return (
<div className="Main">
<Router>
<Menu />
//**Mypage ← only logged in can watch
<Route exact path="/mypage" component={Mypage} />
<Route path="/main" component={Hoge} />
</Router>
</div>
);
}
}
export default Main;
▼mypage.tsx (looged in users can watch)
import React from 'react';
import { RouteComponentProps, Link } from 'react-router-dom';
import { withAuthenticator } from 'aws-amplify-react';
import Auth from '#aws-amplify/auth';
import AuthContext from '../context/auth-context';
interface MatchParams {
id: number;
}
interface State {
user: '',
}
class Mypage extends React.Component<RouteComponentProps<MatchParams>, State> {
constructor(props: RouteComponentProps) {
super(props);
this.state = { user: '' };
}
async componentDidMount() {
let user = await Auth.currentAuthenticatedUser()
this.setState({user: user.username});
}
//**(it's just what I want now)
//loggedIn() {
// console.log("logged in!");
//}
render() {
return (
<div className="mypage">
<h1>mypage</h1>
<div>
<ul>
<li><Link to='hoge'>hoge</Link></li>
</ul>
</div>
</div>
);
}
}
export default withAuthenticator(Mypage);
Answer to J.Hesters
・your ideas 1,2
actually I thought about these ideas(creating signIn form myself), but I wanted to know how it works without them (i should have write about it at first time)
・your idea 3
I inserted console.log("login in!!") in componentDidUpate() but it didnt work after login
Maybe I gotta use the way 1 or 2, but if you get why it doesnt work ur idea3, plz write it here Anyway thx for answering sir ^^
I may be late to answer but you should be using Amplify Logger.
You can then launch whatever code when you need to based on when the event happens. I would do something like this.
import { Hub, Logger } from 'aws-amplify';
const logger = new Logger('Logger', 'INFO');
const listener = (data) => {
switch (data.payload.event) {
case 'signIn':
logger.info('user signed in');
break;
case 'signUp':
logger.info('user signed up');
break;
case 'signOut':
logger.info('user signed out');
break;
case 'signIn_failure':
logger.info('user sign in failed');
break;
case 'configured':
logger.info('the Auth module is configured');
break;
default:
logger.error('Something went wrong, look at data object', data);
}
}
Hub.listen('auth', listener);
You can just write whatever code you want to execute after the await keyword.
async componentDidMount() {
let user = await Auth.currentAuthenticatedUser();
this.setState({user: user.username});
console.log('logged in.');
}
Edit:
As far as I know, you can't explicitly overwrite withAuthenticator's methods. So you have three options as far as I'm concerned:
Supply a custom <SignIn /> component to with authenticator in which you handle the login process manually and invoke whatever function you like as soon as the login method finishes.
Write the whole UI login UI yourself and use Amplify's Auth methods explicitly whenever you need to. Here is an example repository doing it.
Use componentDidUpdate() to trigger code after the component rerenders when the user logs in. Be careful to not create infinite loops with setState. componentDidUpdate only gets called, when the component rerenders. Components within withAuthenticator don't neccesarily rerender.

Reset store after logout with Apollo client

I'm trying to reset the store after logout in my react-apollo application.
So I've created a method called "logout" which is called when I click on a button (and passed by the 'onDisconnect' props).
To do that I've tried to follow this example :
https://www.apollographql.com/docs/react/recipes/authentication.html
But in my case I want LayoutComponent as HOC (and it's without graphQL Query).
Here is my component :
import React, {Component} from 'react';
import { withApollo, graphql } from 'react-apollo';
import { ApolloClient } from 'apollo-client';
import AppBar from 'material-ui/AppBar';
import Sidebar from 'Sidebar/Sidebar';
import RightMenu from 'RightMenu/RightMenu';
class Layout extends Component {
constructor(props) {
super(props);
}
logout = () => {
client.resetStore();
alert("YOUHOU");
}
render() {
return (
<div>
<AppBar title="myApp" iconElementRight={<RightMenu onDisconnect={ this.logout() } />} />
</div>
);
}
}
export default withApollo(Layout);
The issue here is that 'client' is not defined and I can't logout properly.
Do you have any idea to help me to handle this situation or an example/best practices to logout from apollo client ?
Thanks by advance
If you need to clear your cache and don't want to fetch all active queries you can use:
client.cache.reset()
client being your Apollo client.
Keep in mind that this will NOT trigger the onResetStore event.
client.resetStore() doesn't actually reset the store. It refetches all
active queries
Above statement is very correct.
I was also having the logout related problem. After using client.resetStore() It refetched all pending queries, so if you logout the user and remove session token after logout you will get authentication errors.
I used below approach to solve this problem -
<Mutation
mutation={LOGOUT_MUTATION}
onCompleted={()=> {
sessionStorage.clear()
client.clearStore().then(() => {
client.resetStore();
history.push('/login')
});
}}
>
{logout => (
<button
onClick={() => {
logout();
}}
>
Logout <span>{user.userName}</span>
</button>
)}
</Mutation>
Important part is this function -
onCompleted={()=> {
sessionStorage.clear(); // or localStorage
client.clearStore().then(() => {
client.resetStore();
history.push('/login') . // redirect user to login page
});
}}
you can use useApolloClient to access apollo client.
import { useApolloClient } from "#apollo/client";
const client = useApolloClient();
client.clearStore();
You were very close!
Instead of client.resetStore() it should have been this.props.client.resetStore()
withApollo() will create a new component which passes in an instance
of ApolloClient as a client prop.
import { withApollo } from 'react-apollo';
class Layout extends Component {
...
logout = () => {
this.props.client.resetStore();
alert("YOUHOU");
}
...
}
export default withApollo(Layout);
For those unsure about the differences between resetStore and clearStore:
resetStore()
Resets your entire store by clearing out your cache and then
re-executing all of your active queries. This makes it so that you may
guarantee that there is no data left in your store from a time before
you called this method.
clearStore()
Remove all data from the store. Unlike resetStore, clearStore will not
refetch any active queries.

Prevent routing in React when user manually changes url in browser tab

I am stuck in a issue that happens when user manually changes the route in browser tab and presses enter. This forces my react router to navigate to the state entered by user. I want to prevent this and allow routing only through the flow I have implemented by button clicks in my website.
Some of my screens need data that will be available only if the user navigates the site using the flow expected. If user directly tries to navigate to a particular route by manually changing the route in url then he may skip the desired flow and hence the app will break.
Other scenario, in case I want to restrict some users from accessing some routes but the user knows the path and manually enters that in browser url then he will be presented with that screen but should not be.
What I do is use a prop from previous page, if that prop is undefined(meaning user did not follow due process :) hehe ) I simply send the user back to the landing page or wherever.
You can create a route guard using HOC. For example, you don't want unauthorized user to pass route /profile, then you can do the following:
// requireAuthorized.js (HOC)
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {Redirect} from 'react-router-dom'
const connector = connect(
state => ({
isAuthorized: state.profile !== null // say, you keep user profile in redux
})
)
export default (WrappedComponent) => {
return (
connector(
class extends Component {
static propTypes = {
isAuthorized: PropTypes.bool.isRequired
}
render () {
const {isAuthorized, ...clearedProps} = this.props
if (isAuthorized) {
return <WrappedComponent {...clearedProps} />
} else {
return <Redirect to={{pathname: '/login'}} />
}
}
}
)
)
}
// ProfilePage.jsx
import React from 'react'
...
import requireAdmin from '../hocs/requireAdmin' // adjust path
class ProfilePage extends React.Component {
...
render () {
return (
<div>
...
</div>
)
}
}
export default requireAdmin(ProfilePage)
Pay attention to the export statement in my ProfilePage.js
I'd suggest using this library for cleanest solution (or at least make personal similar implementation of it).
Then you'd create authentication check HOC:
export const withAuth = connectedReduxRedirect({
redirectPath: '/login',
authenticatedSelector: state => state.user.isAuthenticated, // or whatever you use
authenticatingSelector: state => state.user.loading,
wrapperDisplayName: 'UserIsAuthenticated'
});
And you could easily create flow HOC:
export const withFlow = (step) = connectedReduxRedirect({
redirectPath: '/initial-flow-step',
authenticatedSelector: state => state.flow[step] === true,
wrapperDisplayName: 'FlowComponent'
});
Then initialize your component
const AuthenticatedComponent = withAuth(Dashboard)
const SecondStepComponent = withFlow("first-step-finished")(SecondStep)
const ThirdStepComponent = withFlow("second-step-finished")(ThirdStep)
You can easily create authenticated flow step by composing HOC:
const AuthSecondStepComponent = withAuth(withFlow("first-step-finished")(SecondStep))
Only thing that is important is that you update your redux state correctly as going through your step flow. When user finishes first step you'd set
state.flow["first-step-finished"] = true // or however you manage your state
so that when user navigates manually to specific page, he wouldn't have that redux state because its an in-memory state and would be redirected to redirectPath route.
Something like this is suitable. You make HOC Route with a wrap to function that deals with authentication/context props.
Note: this deals with direct access to the route, not to the menu items and such. That must be treated in a simmilar way on the menu / menuItem components.
import requireAuth from "../components/login/requireAuth";
class Routes extends React.Component<RoutesProps, {}> {
render() {
return (
<div>
<Switch>
<Route exact={true} path="/" component={requireAuth(Persons, ["UC52_003"])} />
<Route path="/jobs" component={requireAuth(Jobs, ["UC52_006"])} />
</Switch>
</div>
)
}
}
export default function (ComposedComponent, privileges) {
interface AuthenticateProps {
isAuthenticated: boolean
userPrivileges: string[]
}
class Authenticate extends React.Component<AuthenticateProps, {}> {
constructor(props: AuthenticateProps) {
super(props)
}
render() {
return (
isAuthorized(this.props.isAuthenticated, privileges, this.props.userPrivileges) &&
<ComposedComponent {...this.props} /> || <div>User is not authorised to access this page.</div>
);
}
}
function mapStateToProps(state) {
return {
isAuthenticated: state.userContext ? state.userContext.isAuthenticated : false,
userPrivileges: state.userContext ? state.userContext.user ? state.userContext.user.rights : [] : []
};
}
return connect(mapStateToProps, null)(Authenticate);
}
you can put the condition in useEffect of the given page/screen and push it back if it doesnt have the required values.. example below

React-Router v4 and Redux authentication

I'm using react-router v4 and Redux for a new project.
I have the following code:
export class App extends Component {
componentDidMount() {
const { dispatch } = this.props;
dispatch(initAuth());
}
render() {
const { user } = this.props;
return (
<BrowserRouter>
<div>
<NavContainer />
<Match pattern="/login" component={LogInForm} />
<MatchWhenAuthorized pattern='/users' component={Users} user={user} />
</div>
</BrowserRouter>
);
}
}
Where initAuth dispatches an action that checks if there's an existing token in localStorage and if there is, a logIn action is dispatched as well.
The problem is that if I go directly to myapp.com/users the action hasn't returned yet, so there's no user logged in and in that case MatchWhenAuthorized redirects me to my LogInForm, which I don't want if my initAuth logs a user in.
Is there an elegant way to solve this?
I think I could solve it by rendering the MatchWhenAuthorized component only if there's a user logged in, but that doesn't seem right.
The initial login state should be set when the page is loaded and before you mount your app. I'm not sure why the initAuth is a redux action creator when you could just check the localStorage without involving redux.
index.js
import { createStore } from 'redux'
import reducer from './reducer'
import { getUser } from './storage'
// load the user data from localStorage and
// use the value in the store's initial data
const store = createStore(reducer, {
user: getUser()
})
Then you can connect your <MatchWhenAuthorized> component to the store to access the user value and redirect if there is no user in the store.

Resources