MSAL authentication and authrization with React.js - javascript

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} />
);
};

Related

How to persist authentication after reload using react auth kit

I am new to the concept of authentications in apps, especially using tokens. I found the react-auth-kit library to help me do the authentication. I have a simple login using a username and a password with a set backend that works well on Postman. I managed to also authenticate the user into the dashboard, but when I reload the page, the user is sent back to the login page.
I tried using sessionStorage which someone pointed out as a security risk in a blog and didn't succeed either. I did not see the concept in the documentation. Could someone point me in the right direction, maybe a better library or a workaround on this one?
// In my app component...
import Login from "./components/Login";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Dashboard from "./pages/Dashboard";
import { useIsAuthenticated } from 'react-auth-kit'
import { useEffect, useState } from "react";
function App() {
const redirect = () => {
if (isAuthState) {
return <Dashboard />
} else {
return <Login />
}
}
return (
<BrowserRouter>
{/* <Login /> */}
<Routes>
<Route path='/' element={ <Login /> } />
<Route path='/Dashboard' element={redirect()} />
</Routes>
</BrowserRouter>
);
}
// In my Login component this is the handler for the form. I used react-hook-form for validation...
const signIn = useSignIn()
const navigate = useNavigate()
const login: SubmitHandler<Inputs> = (data) => {
axios.post<SignInType>('http://127.0.0.1:8000/api/login', data)
.then((res) => {
if(res.data.status === 200) {
if(signIn({token: res.data.token, tokenType: 'Bearer', expiresIn: 300000})) {
navigate('/dashboard')
}
} else {
setCredentialsError('Invalid credentials, please try again...')
}
})
};

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>

Is getting currently authenticated user an asynchronous call?

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')
);

How to correctly redirect user after authentication?

I am using react-redux and currently I have a bool isAuthenticated property in my global state and if this is true it will redirect to /home component.
Is there a better approach to the one I implemented? How can I resolve the output error?
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { login } from "./actions";
import { useHistory } from "react-router-dom"
const Login = () => {
const dispatch = useDispatch();
const history = useHistory();
const myState = useSelector(
state => state.authentication
);
const onSubmit = (values) => {
dispatch(login(values.email, values.password));
};
useEffect(() => {
if (myState.isAuthenticated) {
history.push("/home")
}
}, [isAuthenticated]);
}
Warning: React Hook useEffect has a missing dependency: 'history'. Either include it or remove the dependency array
[isAuthenticated, history] is this acceptable?
Is there a better approach to the one I implemented? How can I resolve the output error?
The answer depends on where you keep your logic. If you keep all your logic in components' states, the useEffect way is perfectly fine.
To resolve the warning, just put the history to the useEffect inputs as below:
useEffect(() => {
if (myState.isAuthenticated) {
history.push("/home")
}
}, [myState.isAuthenticated, history]);
Now the useEffect will be triggered every time the reference to the history object changes. If you're using React Router, then the history instance is most probably mutable and it will not change, and hence not trigger the useEffect. So it should be safe to include it in the UseEffect dependency array.
The better way is always create separate PrivateRoute component which handle your authentication. If you go through react router official documentation they have given a good example of authentication:
function PrivateRoute({ children, ...rest }) {
const myState = useSelector(
state => state.authentication
);
return (
<Route
{...rest}
render={({ location }) =>
myState.isAuthenticated ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
);
}
You can use wrap it in layer like this:
<Switch>
<Route path="/public">
<PublicPage />
</Route>
<Route path="/login">
<LoginPage />
</Route>
<PrivateRoute path="/protected">
<ProtectedPage />
</PrivateRoute>
</Switch>
In that way you dont have to manually track your authentication.
Here is complete demo from react-router website:https://reactrouter.com/web/example/auth-workflow

How to authenticate user in gatsby

I have followed this tutorial to implement authentication in my gatsby project. The problem is I have first setup the project and the routing is made from the pages folder and then I have implemented the above auth code but it still taking the routes from the pages folder and not from the app.js file. Could someone please help how can I route my components from the app.js instead of using from pages folder.
This is my gatsby-nodejs file
// Implement the Gatsby API “onCreatePage”. This is
// called after every page is created.
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
// page.matchPath is a special key that's used for matching pages
// only on the client.
if (page.path.match(/^\/app/)) {
page.matchPath = "/app/*"
// Update the page.
createPage(page)
}
}
here is src/pages.app.js
import React from "react"
import { Router } from "#reach/router"
import Layout from "../components/layout"
import Home from '../components/dashboard/home/container'
import Login from '../components/marketing/home/pulsemetrics'
import { isLoggedIn } from "../services/auth"
console.log('vvvvvvvvvvvvvvvvvvvvv')
const PrivateRoute = ({ component: Component, location, ...rest }) => {
console.log('hjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjiiiiiiiiiiiiiiiiiii')
if (!isLoggedIn() && location.pathname !== `/app/login`) {
// If the user is not logged in, redirect to the login page.
navigate(`/app/login`)
return null
}
return <Component {...rest} />
}
const App = () => (
<Layout>
<Router>
<PrivateRoute path="/ddddddddddddddddddd" component={Home} />
<Login path="/" />
</Router>
</Layout>
)
export default App
The paths that you have in your App.js should have /app/ prepended in front of them since your PrivateRoute logic uses that to check for a login. Furthermore what your gatsby-node.js file is really saying is that for routes starting with app it should create a new page. Your src/pages/app.js has the task to define how these pages should be created (since they won't be the usual generated static pages by gatsby)
import React from "react"
import { Router } from "#reach/router"
import Layout from "../components/layout"
import Home from '../components/dashboard/home/container'
import Login from '../components/marketing/home/pulsemetrics'
import { isLoggedIn } from "../services/auth"
console.log('vvvvvvvvvvvvvvvvvvvvv')
const PrivateRoute = ({ component: Component, location, ...rest }) => {
console.log('hjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjiiiiiiiiiiiiiiiiiii')
if (!isLoggedIn() && location.pathname !== `/app/login`) {
// If the user is not logged in, redirect to the login page.
navigate(`/app/login`)
return null
}
return <Component {...rest} />
}
const App = () => (
<Layout>
<Router>
<PrivateRoute path="/app/home" component={Home} />
<Login path="/app/login" />
</Router>
</Layout>
)
export default App
Read the gatsby client-only routes documentation for reference or have a look at this github issue

Categories