React authentication with private routes and context - reactjs

I'm trying to add simple authentication to a React+Typescript app using private routes and context. I have a simple login component with a button that just sets a boolean var authenticated in the context to true. The private routes should check this var and redirect to the login component if it's not true otherwise show the specified component. The problem is authenticated seems to always be false and I'm always redirected to the login page.
When I debug it I can see the setAuthenticated function in AuthContextProvider is called when the login button is clicked. But if I then click any of the links to private routes authenticated is always false.
Here's my App.tsx:
function App() {
return (
<AuthContextProvider>
<Router>
<Link to="/">Home</Link>
<Link to="/projects">Projects</Link>
<div>
<Route path="/login" component={Login} />
<PrivateRoute path="/" exact component={Home} />
<PrivateRoute path="/projects" component={Projects} />
</div>
</Router>
</AuthContextProvider>
);
}
export default App;
PrivateRoute.tsx:
interface PrivateRouteProps extends RouteProps {
// tslint:disable-next-line:no-any
component: any;
}
const PrivateRoute = (props: PrivateRouteProps) => {
const { component: Component, ...rest } = props;
return (
<AuthContextConsumer>
{authContext => authContext && (
<Route {...rest}
render={ props =>
authContext.authenticated === true ? (
<Component {...props} />
) : (
<Redirect to="/login" />
)
}
/>
)}
</AuthContextConsumer>
);
};
export default PrivateRoute;
AuthContext.tsx:
export interface AuthContextInterface {
authenticated: boolean,
setAuthenticated(newAuthState: boolean):void
}
const ctxt = React.createContext<AuthContextInterface>({
authenticated: false,
setAuthenticated: () => {}
});
export class AuthContextProvider extends React.Component {
setAuthenticated = (newAuthState:boolean) => {
this.setState({ authenticated: newAuthState });
};
state = {
authenticated: false,
setAuthenticated: this.setAuthenticated,
};
render() {
return (
<ctxt.Provider value={this.state}>
{this.props.children}
</ctxt.Provider>
);
}
}
export const AuthContextConsumer = ctxt.Consumer;
Login.tsx:
function Login() {
return (
<AuthContextConsumer>
{({ authenticated, setAuthenticated }) => (
<div>
<p>Login</p>
<form>
<input type="text" placeholder="Username"/>
<input type="password" placeholder="Password"/>
<button onClick={event => {
setAuthenticated(true);
}}>Login</button>
</form>
</div>
)}
</AuthContextConsumer>
);
}
export default Login;
My suspicious is that there's something wrong with the state definition in AuthContextProvider. If I change authenticatedin here to true I see the opposite behaviour, I never see the login page. Should this be something dynamic?

Or, in the onClick callback, set event.preventDefault() so it doesn't submit the form.

The problem turned out to be that the app was reloading every time the login button was pressed, and therefore lost the state in the AuthContext.
The reason for this is that in my Login component I had a button inside a form, which automatically submits the form and reloads the page.
The solution is to either remove the form tags, or in the button specify the attribute type="button".

Related

React with TypeScript - Authorization and component visibility depending on user claims

I am working on a react application.
I am trying to create login and register functionality.
I have a Authorized.tsx component which looks like this
export const Authorized = (props: authorizedProps) => {
const [isAuthorized, setIsAuthorized] = useState(true);
const { claims } = useContext(AuthContext);
useEffect(() => {
if (props.role) {
const index = claims.findIndex(
claim => claim.name === 'role' && claim.value === props.role)
setIsAuthorized(index > -1);
} else {
setIsAuthorized(claims.length > 0);
}
}, [claims, props.role]);
return (
<>
{isAuthorized ? props.authorized : props.notAuthorized}
</>
);
};
interface authorizedProps {
authorized: ReactElement;
notAuthorized?: ReactElement;
role?: string;
}
This component hides and shows diffrent kind of components depending on if the user is authorized or not.
I am using this component to only show the Login.tsx component for users that are not logged in. I dont want anyone who is not logged in to be able to visit the website.
In my Index.tsx I am using the Authorized.tsx component like this
const Index = () => {
const [claims, setClaims] = useState<claim[]>([
// { name: "email", value: "test#hotmail.com" },
]);
return (
<div>
<BrowserRouter>
<AuthContext.Provider value={{ claims, update: setClaims }}>
<Authorized authorized={<App />} notAuthorized={<Login />} />
</AuthContext.Provider>
</BrowserRouter>
</div>
);
};
All the authorized users will be able to visit the site, everyone else will be asked to log in.
However, the problem I have is when I tried adding the Register.tsx component into the Login.tsx component as a navigational link.
I wish to be able to navigate between Register and Login
This is how the Login.tsx component looks like
export const Login = () => {
return (
<>
<h3>Log in</h3>
<DisplayErrors errors={errors} />
<AuthForm
model={{ email: "", password: "" }}
onSubmit={async (values) => await login(values)}
BtnText="Log in" />
<Switch>
<Route path="/register">
<Register />
</Route>
<Link to='/register'>Register</Link>
</Switch>
</>
);
};
But what actually happends when I press the 'Register' link is that the Register component gets added below the Login component
Before pressing the 'Register' link
After pressing the 'Register' link
I understand it has something to do with the Authorized.tsx component in Index.tsx.
That I am telling it to only show the Login component when not authorized.
But I dont know how I could fix it so I will be able to navigate between the Login and the Register
All help I could get would be much appreciated!
Thanks
With the current implementation you are rendering a Login component that then also renders a route for a Register component to be rendered on. Login remains mounted and rendered the entire time. From what you describe you want to render Login and Register each on their own route.
Abstract both these components into a parent component that manages the route matching and rendering.
Example
const Unauthenticated = () => (
<Switch>
<Route path="/register" component={Register} />
<Route component={Login} />
</Switch>
);
...
export const Login = () => {
...
return (
<>
<h3>Log in</h3>
<DisplayErrors errors={errors} />
<AuthForm
model={{ email: "", password: "" }}
onSubmit={login}
BtnText="Log in"
/>
<Link to='/register'>Register</Link>
</>
);
};
...
const Index = () => {
const [claims, setClaims] = useState<claim[]>([
// { name: "email", value: "test#hotmail.com" },
]);
return (
<div>
<BrowserRouter>
<AuthContext.Provider value={{ claims, update: setClaims }}>
<Authorized
authorized={<App />}
notAuthorized={<Unauthenticated />}
/>
</AuthContext.Provider>
</BrowserRouter>
</div>
);
};

Redirect to last browser history location in React

I am working on my personal React project.
The React app has a login page. After successfully logging in, the user is redirected to homepage i.e. on path / . And if the user again tries to navigate to login page i.e. path
/login , then the user will be redirected back to homepage
The app also has protected route on path /profile which is accessible only if the user is authenticated.
But the problem is, when the user navigates to protected route and then refreshes the page, it is redirecting to the homepage.
How can I make it redirect back to the same protected route from where the user had refreshed?
Here is Login.js
// Login.js
if (isUserLoggedIn) {
return <Redirect to="/" />;
}
// else render login page
And here is ProtectedRoute.js
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { Redirect, Route } from "react-router";
export default function ProtectedRoute({ component: Component, ...rest }) {
const { isUserLoggedIn } = useSelector((state) => state.common);
return (
<Route
{...rest}
render={(props) =>
!isUserLoggedIn ? <Redirect to="/login" /> : <Component {...props} />
}
/>
);
}
And this is how I'm getting the value of isUserLoggedIn from cookies and then setting it to the Redux state.
const [authenticated] = useState(() =>
Boolean(Number(Cookies.get("isUserLoggedIn")))
);
useEffect(() => {
if (!authenticated) {
return dispatch({
type: NOT_AUTHENTICATED,
});
} else {
return dispatch({
type: AUTHENTICATED,
});
}
}, [authenticated]);
In simpler terms, you will have to make the browser remember that the user is now authenticated. That can be simply achieved by adding a specific token or isValid = true to the session storage and then you will have to use the value from session storage as your reference.
Updating user status in the state cannot persist it on reload. You would have to use persistor then. However, the easier way is to update session storage.
You can create a new JS file, say session.js and create the following functions:
export const setToken = (value) => {
sessionStorage.setItem('token', value)
}
export const getToken = () => {
return sessionStorage.getItem('token') || null
}
export const removeToken = () => {
sessionStorage.removeItem('token')
}
You can call setToken(true) on successful authentication and remove the token removeToken() on log out. You can then call getToken() to check your user's authentication in your if statement. I hope it helps!
It happened same with me. I have solved this problem by using a variable that will be in Local storage so when siging in also set localstorage variable isLogged to true localStorage.setItem("isLogged", true) so in case he is already logged and refreshed the page the user will redirected to profile page.
The switch statement is used bacause we get string value from localstorage and we have to convert it to boolean
function App() {
const token = localStorage.getItem("isLogged");
let auth = null;
switch (token) {
case "true":
auth = true;
break;
case "false":
auth = false;
break;
default:
break;
}}
as the code above you can now pass the variable auth to the component in protected route like this
<PrivateRoute
path="/main"
component={Main}
isAuthenticated={auth}
></PrivateRoute>
and in the Private Route Component add the isAuthenticated props
const PrivateRoute = ({ component: Component, isAuthenticated, ...props }) => {
return (
<Route
{...props}
render={(props) =>
isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
}}
/>
)
}
/>
);
};
This is the Login Component
const Login = () => {
const history = useHistory();
async function onSubmit(data) {
try {
await signin(data.userName, data.password);
localStorage.setItem("isLogged", true);
toast.success("Registerd successfully");
history.go("/main");
} catch {
toast.error("Error");
}
}
return (
<div className="form-container">
<img src={Logo} alt="logo" className="logo-img"></img>
<h1>Login to Your Account</h1>
<Form onSubmit={onSubmit} className="form-items">
{({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<Field name="userName">
{({ input }) => (
<TextField
name="userName"
type="input"
id="standard-basic"
label="user Name"
variant="standard"
fullWidth
{...input}
/>
)}
</Field>
<Field name="password">
{({ input }) => (
<TextField
name="password"
type="password"
id="standard-basic"
label="Password"
variant="standard"
fullWidth
{...input}
/>
)}
</Field>
<Button
type="submit"
sx={{
width: "100%",
backgroundColor: "rgba(56, 211, 159, 255)",
marginTop: "40px",
color: "white",
}}
>
Login
</Button>
<div>
Don't Have an Account <Link to="/register">Sign up</Link>
</div>
</form>
)}
</Form>
</div>
);
};
You are using Redux, but don't appear to load your auth state from persisted storage and instead use a mounting useEffect hook to dispatch an action with an auth status read from an isUserLoggedIn cookie.
useEffect runs at the end of the initial render cycle, so during the initial render the isUserLoggedIn is likely a falsey default initial reducer value.
In this case your ProtectedRoute component should wait until the isUserLoggedIn state is actually populated.
export default function ProtectedRoute(props) {
const { isUserLoggedIn } = useSelector((state) => state.common);
if (isUserLoggedIn === undefined) { // or whatever the default, indeterminant value is
return null; // or loading indicator, etc...
}
return isUserLoggedIn ? <Route {...props} /> : <Redirect to="/login" />;
}

User Context values doesnt change in child component

I am trying to redirect user in case user is not authenticated and vice versa
so, I have the directory structure as follow
myproject
src
App.js
UserContext.js
routes
index.js
route.js
pages
Dashboard
index.js
authentication
login.js
In my app.js i do a call and get my authentication token
and set auth to true and pass it in user context but it has the default values and i cannot redirect currently redirecting with only window.location.href
my code for usercontext.js
import { createContext } from "react";
export const UserContext = createContext(null)
APP.js
const App = props => {
const [user,setUser] = React.useState(null)
var [auth,setAuth] = React.useState(false)
const isAuthenticated = ()=>
{
var isAdmin = true;
axios.get(`/verifyToken`).then((response)=>{
console.log(response.data.auth)
setUser({...response.data.user})
setAuth(response.data.auth)
console.log(response.data.user)
})
}
useEffect(() => {
isAuthenticated()
console.log(auth)
},[]);
function getLayout() {
let layoutCls = VerticalLayout
switch (props.layout.layoutType) {
case "horizontal":
layoutCls = HorizontalLayout
break
default:
layoutCls = VerticalLayout
break
}
return layoutCls
}
const Layout = getLayout()
return (
<React.Fragment>
<Router>
<Switch>
<UserContext.Provider value={{user,setUser,auth,setAuth,isAuthenticated}}>
{publicRoutes.map((route, idx) => (
<Authmiddleware
path={route.path}
layout={NonAuthLayout}
component={route.component}
key={idx}
isAuthProtected={auth}
exact
/>
))}
{authProtectedRoutes.map((route, idx) => (
<Authmiddleware
path={route.path}
layout={Layout}
component={route.component}
key={idx}
isAuthProtected={auth}
exact
/>
))}
</UserContext.Provider>
</Switch>
</Router>
</React.Fragment>
)
}
My index.js file has component and routes names array which i am looping above
and this is my route.js
const Authmiddleware = ({
component: Component,
layout: Layout,
isAuthProtected,
...rest
}) => (
<Route
{...rest}
render={props => {
return (
<Layout>
<Component {...props} />
</Layout>
)
}}
/>
)
Authmiddleware.propTypes = {
isAuthProtected: PropTypes.bool,
component: PropTypes.any,
location: PropTypes.object,
layout: PropTypes.any,
}
export default Authmiddleware;
So, now If in my dashboard.js I try to access user on wan tto redirect if auth is false it only has default values of user and auth
I am fetching as follows in dashboard.js
import {UserContext} from '../../UserContext'
const {user,setUser,auth,setAuth,isAuthenticated} = React.useContext(UserContext)
React.useEffect(()=>{
if(auth == false){
window.location.href='/login'
//IT TAKES ME LOGIN EVERYTIME AT IT IS ONLY GETTING DEFAULT VALUE THAT IS FALSE
},[])
WHAT I HAVE TRIED
If i place the isAuthenticated() function call in every component it works
but that would be like so many lines of code same in every component
What is the way to go with?
Anyone facing the same issue I resolved it by
bringing out
<UserContext.Provider></UserContext.Provider>
outside the switch
<UserContext.Provider value={{user,setUser,auth,setAuth,isAuthenticated}}>
<Switch>
</Switch>
</UserContext.Provider value={{user,setUser,auth,setAuth,isAuthenticated}}>
I FOUND THE REASON HERE: https://coderedirect.com/questions/324089/how-to-use-context-api-with-react-router-v4
The reason posted in answer here was that Switch expects routes directly.

React redux store's state not saved

I created a store with redux to experiment with the app state management in react. So far I'm just trying to make a fake authentication behavior when clicking on the "sign in" button on the login page, which is working because my isLogged state change to true. But then when I try to access a path that I protected by checking if isLogged is true, I get false... why is the state of isLogged not saved when routing with react-router_dom?
index.js
const store = createStore(
allReducers,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App/>
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
App.js
class App extends Component {
render() {
return (
<Box style={{width: "99.6vw", height: "95.6vh"}}>
<Router>
<SideNavBar/>
<Switch>
<Route exact path={"/"} render={() => <Redirect to={"/login"}/>}/>
<Route path={"/login"} component={LoginPage}/>
<ProtectedRoute path={"/somepage"} component={somePage}/>
</Switch>
</Router>
</Box>
);
}
}
LoginPage.js
class LoginPage extends Component {
render() {
const {dispatch} = this.props;
return (
<LoginPageContainer>
<img src={logo} alt={""} height={"350rem"}/>
<FilledInput placeholder={"Login or email"}/>
<FilledInput placeholder={"Password"}/>
<Button onClick={() => dispatch({ type: "SIGN_IN" })}>
Sign in
</Button>
</LoginPageContainer>
);
}
}
export default connect(null, null)(LoginPage);
ProtectedRoute.js
import {connectProtectedRoute as connect} from "../redux/connectProtectedRoute";
class ProtectedRoute extends Component {
render() {
const {isLogged, component} = this.props;
return (
<Route render={
() => {
if (isLogged)
return (component);
else
return (<Redirect to={"/login"}/>);
}
}/>
);
}
}
ProtectedRoute.propTypes = {
component: PropTypes.elementType.isRequired
};
export default connect(ProtectedRoute);
connectProtectedRoute.js
import {connect} from "react-redux";
function mapStateToProps(state) {
return ({
isLogged: state.isLogged
});
}
export const connectProtectedRoute = connect(mapStateToProps, null);
reducers.js
const allReducers = combineReducers({
isLogged: isLoggedReducer
});
export default allReducers;
isLoggedReducer.js
const isLoggedReducer = (state = false, action) => {
switch (action.type) {
case "SIGN_IN": return true;
case "SIGN_OUT": return false;
default: return state;
}
}
export default isLoggedReducer;
So I was just unaware of the losing state fact upon refresh. Comment from original post said it all, here they are for anyone ending here:
Modifying the URL manually (outside of react router) will cause a full page refresh and all state will be lost (unless you persist it in local storage or by some other method). This is your problem, nothing in the code looks wrong. – Brian Thompson
Modifying the url causes the page refresh and follows the rerunning the app, so all data in store are removed. Try to use history for page navigation. Here is how to use it. reacttraining.com/react-router/web/api/Hooks/usehistory – TopWebGhost

Building Membership based website with React

I want to build a membership-based web app using React. The users would sign-up and pay before they can access exclusive content. If they have already registered or signed up they can just login and access the content.
How would I go about making such a web app with React? What would I need to implement?
You can use react-router, found in npm packages : https://www.npmjs.com/package/react-router
See this example to private routes https://reacttraining.com/react-router/web/example/auth-workflow
import React from "react";
import {
BrowserRouter as Router,
Route,
Link,
Redirect,
withRouter
} from "react-router-dom";
////////////////////////////////////////////////////////////
// 1. Click the public page
// 2. Click the protected page
// 3. Log in
// 4. Click the back button, note the URL each time
const AuthExample = () => (
<Router>
<div>
<AuthButton />
<ul>
<li>
<Link to="/public">Public Page</Link>
</li>
<li>
<Link to="/protected">Protected Page</Link>
</li>
</ul>
<Route path="/public" component={Public} />
<Route path="/login" component={Login} />
<PrivateRoute path="/protected" component={Protected} />
</div>
</Router>
);
const fakeAuth = {
isAuthenticated: false,
authenticate(cb) {
this.isAuthenticated = true;
setTimeout(cb, 100); // fake async
},
signout(cb) {
this.isAuthenticated = false;
setTimeout(cb, 100);
}
};
const AuthButton = withRouter(
({ history }) =>
fakeAuth.isAuthenticated ? (
<p>
Welcome!{" "}
<button
onClick={() => {
fakeAuth.signout(() => history.push("/"));
}}
>
Sign out
</button>
</p>
) : (
<p>You are not logged in.</p>
)
);
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
fakeAuth.isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);
const Public = () => <h3>Public</h3>;
const Protected = () => <h3>Protected</h3>;
class Login extends React.Component {
state = {
redirectToReferrer: false
};
login = () => {
fakeAuth.authenticate(() => {
this.setState({ redirectToReferrer: true });
});
};
render() {
const { from } = this.props.location.state || { from: { pathname: "/" } };
const { redirectToReferrer } = this.state;
if (redirectToReferrer) {
return <Redirect to={from} />;
}
return (
<div>
<p>You must log in to view the page at {from.pathname}</p>
<button onClick={this.login}>Log in</button>
</div>
);
}
}
export default AuthExample;

Resources