Is getting currently authenticated user an asynchronous call? - javascript

If I get currently authenticated user through auth.currentUser, is this an asynchronous cal and be handled as such?
Namely, I have this top-level App component in react with firebase on the backend.
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import MainNav from './layouts/Navbar.js';
import Homepage from './pages/Homepage';
import Login from './pages/Login';
import Register from './pages/Register';
import Profile from './pages/Profile';
import Question from './pages/Question';
import auth from 'auth/path/from/firebase-config'
function App() {
const user = auth.currentUser
return (
<>
<BrowserRouter>
<MainNav />
<Routes>
<Route path="/" element={<Homepage />} />
<Route path="/questions/:id" element={<Question />} />
<Route path="/users/:id" element={<Profile />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
</Routes>
</BrowserRouter>
</>
);
}
export default App;
I would like to know if there is an authenticated user and tell that to the . Depending on whether there is a user I want to show register, and login buttons or logout button (or something like that)
Is there maybe a way here to utilize firebase's onAuthStateChanged observer?

Firebase automatically restores the user credentials when the page/app reloads. This requires it to make a call to the server, so happens asynchronously. This call likely hasn't completed when your auth.currentUser runs, which means you get null for the current user.
The solution is indeed as you say to use an auth state listener, which fires for the first time after the asynchronous call has compelte.

Yes, you must wait onAuthStateChanged to get it ready.
In my app, I created a top-level component to handle this case.
This component blocks render, until firebase get ready.
import { useCallback, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchProfileDetails, handleAuthStateChanged } from '../../redux/actions/auth';
import { getAuth, onAuthStateChanged } from 'firebase/auth'
const auth = getAuth()
export default function AuthGate({ children }) {
const dispatch = useDispatch()
const authStatus = useSelector(state => state.auth.status) //idle (default, onAuthStateChanged not yet fired) || authenticated || unauthenticated
const profileStatus = useSelector(state => state.auth.profile.query.status) //idle || loading || succeeded || failed.
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, user => {
dispatch(handleAuthStateChanged(user)) // this line is responsible to change authStatus from `idle` to `authenticated` || `unauthenticated`
})
return () => {
unsubscribe()
}
}, [dispatch]);
useEffect(() => {
if (authStatus === 'authenticated') {
dispatch(fetchProfileDetails()) //fetch profile details from backend each user authenticated
} else {
// clear profile details each logout
}
}, [authStatus, dispatch]);
const renderChildren = useCallback(() => {
if (authStatus === 'idle' || profileStatus === 'idle' || profileStatus === 'loading') {
//show loading component
} else if (profileStatus === 'failed') {
//show error component
} else {
return children
}
}, [authStatus, children, profileStatus])
return renderChildren()
}
index.js
//...
ReactDOM.render(
//...
<AuthGate>
<App />
</AuthGate>
//...
,
document.getElementById('root')
);

Related

React PrivateRoute auth route

I am working on a basic react auth app, right now the routes /signup and /login work when I run this repo with my .env.local file that contains firebase auth variables.
https://github.com/MartinBarker/react-auth-app
I am trying to make it so that the '/' route that points to Dashboard will only be accessible for a user who is currently signed in, and if a user is not signed in but tries to access the '/' route they will be redirected to the '/login' page.
But whenever I use the route
<PrivateRoute exact path="/" element={Dashboard} />
my chrome devtools console shows a blank page with error messages:
index.tsx:24 Uncaught Error: [PrivateRoute] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>
my PrivateRoute.js looks like this:
// This is used to determine if a user is authenticated and
// if they are allowed to visit the page they navigated to.
// If they are: they proceed to the page
// If not: they are redirected to the login page.
import React from 'react'
import { Navigate, Route } from 'react-router-dom'
import { useAuth } from '../Contexts/AuthContext'
const PrivateRoute = ({ component: Component, ...rest }) => {
// Add your own authentication on the below line.
//const isLoggedIn = AuthService.isLoggedIn()
const { currentUser } = useAuth()
console.log('PrivateRoute currentUser = ', currentUser)
return (
<Route
{...rest}
render={props =>
currentUser ? (
<Component {...props} />
) : (
//redirect to /login if user is not signed in
<Navigate to={{ pathname: '/login'}} />
)
}
/>
)
}
export default PrivateRoute
Im not sure why this error is occurring, any help is appreciated
This behaviour seems to have changed in ReactRouter V6 here is the solution we came up with for a project.
Private route
*Re-creating the users question code
import React from 'react'
import { Navigate, Route } from 'react-router-dom'
import { useAuth } from '../Contexts/AuthContext'
const PrivateRoute = ({ children }) => {
// Add your own authentication on the below line.
//const isLoggedIn = AuthService.isLoggedIn()
const { currentUser } = useAuth()
console.log('PrivateRoute currentUser = ', currentUser)
return (
<>
{
currentUser ? (
children
) : (
//redirect to /login if user is not signed in
<Navigate to={{ pathname: '/login'}} />
)
}
</>
)
}
export default PrivateRoute
Typescript
*Our actual code implementation of this issue
const PrivateRoute: React.FC = ({ children }) => {
const navigate = useNavigate();
const { isAuthenticated, isAuthLoading } = useAuth();
const { user, isLoadingUser } = useContext(UserContext);
// Handle users which are not authenticated
// For example redirect users to different page
// Show loader if token is still being retrieved
if (isAuthLoading || isLoadingUser) {
// TODO: show full page loader
return (
<div>Loading...</div>
);
}
// Proceed with render if user is authenticated
return (
<>
{children}
</>
);
};
Router
<Router>
<Routes>
<Route
path={routes.user.accountSignup.path}
element={
<PrivateRoute>
<AccountSignup />
</PrivateRoute>
}
/>
</Routes>
</Router>

React Context - Reset itself to default value

I'm working on react context. I am creating a context to store the user info from the server, it is storing it fine but there is a wired situation here. Whenever I refresh the page, it reset itself to the default value which is in my case undefined and I am not sure why.
Here is the userContext
import React from "react";
const UserContext = React.createContext();
export default UserContext;
Here is the code for UserProvider
import { useEffect, useState, useContext, useMemo } from "react";
import UserContext from "./userContext";
import axios from "axios";
const UserProvider = (props) => {
const [user, setUser] = useState()
const userLogin = () => {
axios.defaults.headers.common = {'Authorization': `Bearer ${localStorage.getItem("token")}`}
axios
.get("/auth/profile")
.then((res)=>{
setUser(res.data)
})
}
const userLogout = () => {
setUser(null)
}
const providerValue = useMemo(() => ({user, userLogin, userLogout}), [user, userLogin, userLogout] )
return(
<UserContext.Provider value={providerValue}>
{ props.children }
</UserContext.Provider>
)
}
export default UserProvider
The purpose of the userLogin function is to get the info of the user. and the userLogout is to reset the user info to null.
Here is how I implemented it in the App.js
function App() {
const [token, setToken] = useState(localStorage.getItem("token"));
return (
<BrowserRouter>
<UserProvider>
<Routes>
<Route exact path="/" element={<Welcome />}></Route>
<Route path="/register" element={<Register />}></Route>
<Route path="/login" element={<Login />}></Route>
<Route path="/goal" element={<UserGoals />}></Route>
</Routes>
</UserProvider>
</BrowserRouter>
);
}
export default App;
I will not bother you with the details of the rest of the application but I will share with you how I use it in other components
const LoginForm = () => {
const {user, userLogin, userLogout} = useContext(UserContext)
}
Whenever I log in, I just simply call the function userLogin and whenever I want to access the user, I access it using user.
The problem occurs after call userLogin and then refreshing the page, the user becomes undefined. it occurs in multiple pages not only in the login page.
any help would be appreciated.
Most likely it's because you are not calling the userLogin function when the app first runs.
try adding this in UserProvider
useEffect(() => {
if (!user) {
userLogin()
}
}, [])

How can I force the user to go to Login before going to other component?

How can I make the admin go to login first before going to other components? So I've been getting an error about
TypeError: Cannot read properties of null (reading 'isAdmin')
I know where this came from, it's from my
const admin = useSelector((state) => state.user.currentUser.isAdmin)
I was thinking that forcing the admin to redirect to other page, but when I try to use ternary operator, the program itself already calls the useSelector.
Is there any way I can approach this problem?
import { useSelector } from 'react-redux'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import './App.css'
// import Sidebar from './components/sidemenu/Sidebar'
import Home from './pages/home/Home'
import Login from './pages/login/Login'
// import NewProduct from './pages/newProduct/NewProduct'
// import NewUser from './pages/newUser/NewUser'
// import ProductList from './pages/product/ProductList'
// import Product from './pages/productItem/Product'
// import User from './pages/user/User'
// import UserList from './pages/userList/UserList'
function App() {
const admin = useSelector((state) => state.user.currentUser.isAdmin)
// const admin = false
return (
<Router>
<Switch>
<Route path="/">{admin ? <Home /> : <Login />}</Route>
</Switch>
</Router>
)
}
export default App
You should make your code safe.
For modern js you could use the optional chaining operator
const admin = useSelector((state) => state.user?.currentUser?.isAdmin)
A more traditional approach to do the same thing
const admin = useSelector((state) => state.user && state.user.currentUser && state.user.currentUser.isAdmin)
You can first check if currentUser exists.
If currentUser = null that means user is not authenticated and you can redirect them to Login
function App() {
const isLoggedIn = Boolean(useSelector((state) => state.user.currentUser))
const admin = isLoggedIn ? useSelector((state) => state.user.currentUser.isAdmin) : false
return (
<Router>
<Switch>
<Route path="/">{admin ? <Home /> : <Login />}</Route>
</Switch>
</Router>
)
}
you can use react hooks to handle authentication handling in all components.
for example you have context provider like this :
import React, { createContext, useContext, useEffect, useState } from 'react';
export const AuthContext = createContext();
const AuthProvider = ({ children }) => {
const [{ user, isLogin, isAdmin }, updateStatus] = useState({ isLogin: false, isAdmin: false });
// some functions to updates states
useEffect(() => {
if (isLogin && /* user is not in login page */) {
// redirect to login page
}
}, [isLogin])
if (!isLogin || !isAdmin)
return <></>
}
return (
<AuthContext.Provider value={{ user, isAdmin }}>{children}
</AuthContext.Provider>
}
(this context is very basic)
and you can wrap this provider in app.js
You can change
const admin = useSelector((state) => state.user.currentUser.isAdmin)
to
const admin = useSelector((state) => state.user?.currentUser?.isAdmin)
to fix this issue. It definitely works.

How can I protect a route in React router

Bhai log ye adnan ka pucha wa question hai jab m usko react sikha ra tha usko questions etc krne k liye m apna account diya tha cz pehly account verify krwany k liye baqaida coding challange pass krna hota tha (khud ko legit programmer show krwane k liye), Lekin ab kiu k log meri profile dekhty hain tu mujhe embarrassment hoti hai ;(
I'm using React with Node & express and authenticating with passport and passport local, I want to know that how can I protect a route in React, I mean I want to show a component only if user is authenticated and otherwise I want it to redirect on login page.
In my case I want to protect Notes route,
The method I'm trying is not working at all...
My React Code
import React from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
import axios from 'axios';
import Register from './Components/Register';
import Login from './Components/Login';
import Notes from './Components/Notes';
function App () {
//Function to check authentication from server
const checkAuth = () => {
axios.get('http://localhost:8000/notes', { withCredentials : true })
.then(res => res.data)
}
return (
<Switch>
<Route exact path='/' component={Register} />
<Route exact path='/login' component={Login} />
<Route exact path='/notes' render={() => {
return checkAuth()? (
<Notes />
) : (
<Redirect to="/login" />
)
}} />
</Switch>
);
};
export default App;
And My server side code
//Notes Page
router.get('/notes', (req, res) => {
if(req.isAuthenticated()) {
res.send(req.user.email);
}else{
res.send(false);
}
};
Consider showing a loader until the api call returns a value. Once the api call has returned a value then render the required component using a higher order component.
App.jsx
class App extends Component {
state = {
login: false,
loading: false,
}
componentDidMount() {
//Function to check authentication from server
this.setState( {loadnig:true}, () => {
this.checkAuth()
}
}
checkAuth = () => {
// API request to check if user is Authenticated
axios.get('http://localhost:8000/notes', { withCredentials : true })
.then( res => {
console.log(res.data);
// API response with data => login success!
this.setState({
login: true,
loading:false
});
});
.catch( error => {
console.error(error);
// Handle error
this.setState({
loading:false
});
});
}
render() {
let {login, loading} = this.state
let protectedRoutes = null;
if(loading){
return <loader/>
}
return (
<Switch>
//use authorize HOC with three parameters: component, requiresLogin, isLoggedIn
<Route exact path='/path' component={authorize(component, true, login)} />
<Route exact path='/' component={Register} />
<Route exact path='/login' component={Login} />
</Switch>
);
}
};
export default App;
Using a Higher Order Component will give you the flexibility to control routes based on the login status of the users.
Authorize,jsx
export default function (componentToRender, requiresLogin, isLoggedIn) {
class Authenticate extends React.Component<> {
render() {
if (requiresLogin) {
//must be logged in to view the page
if (!isLoggedIn) {
// redirect to an unauthorised page or homepage
this.props.history.push('/unauthorised');
return;
// or just return <unauthoriesd {...this.props} />;
}
} else {
// to be shown to non loggedin users only. Like the login or signup page
if (isLoggedIn) {
this.props.history.push('/unauthorised');
return;
//or just return <home {...this.props} />;
}
}
//if all the conditions are satisfied then render the intended component.
return <componentToRender {...this.props} />;
}
}
return Authenticate;
}
Also, if you ever decide to add in more conditions for the routing then you can easily add those to the HOC.
checking login status via "server request" takes time! Consider then to create a Class with state!, i'll give you an example below:
import React, { Component } from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
import axios from 'axios';
import Register from './Components/Register';
import Login from './Components/Login';
import Notes from './Components/Notes';
class App extends Component {
state = {
login: false
}
componentDidMount() {
//Function to check authentication from server
this.checkAuth()
}
checkAuth = () => {
// API request to check if user is Authenticated
axios.get('http://localhost:8000/notes', { withCredentials : true })
.then( res => {
console.log(res.data);
// API response with data => login success!
this.setState({
login: true
});
});
}
render() {
let protectedRoutes = null;
if(this.state.login === true) {
// if login state is true then make available this Route!
protectedRoutes = <Route exact path='/notes' component={Notes} />
}
return (
<Switch>
<Route exact path='/' component={Register} />
<Route exact path='/login' component={Login} />
{ protectedRoutes }
</Switch>
);
}
};
export default App;
I hope this example will be useful to you.
Your function checkAuth since it makes a network call may not return the correct value in time for rendering, what you SHOULD do is create a state for whether or not the user is authenticated and have checkAuth update that state.
const [authenticated, updateAuthenticated] = useState(false);
...
update your checkAuth to update the authenticated state
//Function to check authentication from server
const checkAuth = () => {
axios.get('http://localhost:8000/notes', { withCredentials : true })
.then(res => updateAuthenticated(res.data)); // double check to ensure res.data contains the actual value returned
}
...
update your rendering to use the state
<Route exact path='/notes' render={() => {
return (authenticated ?
(<Notes />)
:
(<Redirect to="/login" />)
)
}} />
... with this approach you can also set the default in useState to true or false and determine whether or not it's your server side code that's giving trouble
... dont for get to call checkAuth in a useEffect somewhere at the top
useEffect(()=>{
checkAuth()
},[])
Replace your code below
<Route exact path='/notes' render={() => {
checkAuth()? (
<Notes />
) : (
<Redirect to="/login" />
)
}} />
With this code please
<Route exact path="/notes">{checkAuth ? <Notes /> : <Redirect to="/login" />}</Route>

MSAL authentication and authrization with React.js

I am fairly new to React and trying to implement Single Sign On Authentication in my React App.
Objectives:
Provide a login page where the user can enter their email address
On click of Sign-in user get the SSO popup (based Azure AD) to accept the terms and sign-in
Call graph API to retrieve user details (email ID, etc.)
Retrieve the sign in token and store in browser cache (localStorage) and use it for subsequent URL accesses (React routes).
I have come across MSAL (https://github.com/AzureAD/microsoft-authentication-library-for-js) which seems to be useful for this.
What I have tried:
Based on the MSDN docs: https://learn.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-javascript-spa, I have registered my React SPA app in the Azure and got the client ID.
I have created a single js file (Auth.js) to handle sign-in, token generation and graph API call as mentioned in the docs: https://learn.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-javascript-spa#use-the-microsoft-authentication-library-msal-to-sign-in-the-user
In my index.js I have configured the routes:
ReactDOM.render(<MuiThemeProvider theme={theme}>
<Router>
<Switch>
<Route path="/" exact component={Projects}/>
<Route path="/projects" exact component={Projects}/>
<Route path="/admin" exact component={Admin}/>
<Route path="/projectstages/:projectNumber" exact component={ProjectStages}/>
<Route path="/formspage" exact component={FormsPage}/>
<Route path="/users" exact component={UserManagement}/>
</Switch>
</Router>
</MuiThemeProvider>, document.getElementById('root'));
These routes (components) gets rendered within the main App.jsx component:
class App extends Component {
render(){
return(
<div className="App">
{this.props.children}
</div>
);
}
}
How do I integrate this within my React app so that only authenticated users can access the React routes along with the objectives I mentioned above? Please let me know if I can provide more details or explain more about this.
This is usually achieved using higher-order-components.
The idea is, when you load a page that requires authentication, you call an api to get authentication using access token stored from your cookies or whatever storage you use. Then you need to wrap your protected routes to a HOC that checks the authentication data.
import React, {useState, useContext, useRef, createContext } from 'react'
const AuthContext = createContext(null)
export const withAuth = (requireAuth = true) => (WrappedComponent) => {
function Auth(props) {
const isMounted = useRef(false);
// this is the authentication data i passed from parent component
// im just using
const { loading, error, auth } = useContext(AuthContext);
useEffect(() => {
isMounted.current = true;
}, []);
if (!isMounted.current && loading && requireAuth !== 'optional') {
return (<span>Loading...</span>);
}
if ((!auth || error) && requireAuth === true) {
return (<Redirect to="/login" />);
} if (auth && requireAuth === false) {
return (<Redirect to="/" />);
}
return (
<WrappedComponent {...props} />
);
}
return Auth;
};
export function AuthenticationProvider(props) {
const [auth, setAuth] = useState()
const [error, setErr] = usetState()
const [loading, setLoading] = useState(true)
useEffect(() => {
// get authentication here
api.call('/auth')
.then(data => {
setAuth(data)
setLoading(false)
})
.catch(err => {
setLoading(false)
setErr(err)
})
})
return (
<AuthContext.Provider value={{ auth, error, loading }}>
{children}
</AuthContext.Provider>
)
}
Then you can wrap your App with the Authentication Provider
<AuthenticationProvider>
<App/>
</AuthenticationProvider>
And for each of the pages, you use the HOC like this
function ProtectedPage(props){
// your code here
}
export default withAuth(true)(ProtectedPage)
I'd like to recommend to use package for this:
https://www.npmjs.com/package/react-microsoft-login
Install:
yarn add react-microsoft-login
# or
npm i react-microsoft-login
Import and configure component:
import React from "react";
import MicrosoftLogin from "react-microsoft-login";
export default props => {
const authHandler = (err, data) => {
console.log(err, data);
};
return (
<MicrosoftLogin clientId={YOUR_CLIENT_ID} authCallback={authHandler} />
);
};

Categories