How to implement auth in front-end with Redux? - reactjs

I want to know the authentication process on the front end (how and where to save the tokens, how to check if a user is logged in etc), redirect to the login page, when they try to access pages where login is required etc.
I do not want an implementation of this, just libraries to help me, and in case of saving things like tokens, where do I save this?
I am currently learning Redux and have little knowledge, I also saw an article on Saga and it seems to be useful for this authentication process.
As for the back end, I basically need to install some Django extensions and I will have endpoints for things like: enter username / password and return access token, expire an access token, register a user, reset password etc.
For now I know I need Redux and use the Provider and Router of the react-router. Also the basics about actions, reducers, store etc. But nothing more.
Important note: I intend to use hooks instead of class components.

short explanation:
I recommend you to use JWT for auth, you should save token in localStorage
when you login/signup server response a jwt-token:
localStorage.setItem('usertoken', token)
and then you should check user auth:
const isAuth = localStorage.getItem('usertoken')
if you use react-router-4:
if (!isAuth) {
return <Router render={() => <Redirect to="/login" />}
}
// ...your protected routes
Also every api request should contain a jwt-token in api.js file:
const apiResponse = await fetch(url, {
...someOptions,
headers: { 'x-access-token': localStorage.getItem('usertoken') }
})
if server returns 401 response you should delete token:
if (response.status === 401) {
localStorage.removeItem('usertoken');
window.location.href = '/login';
}
An alternative way for auth to use cookies

Related

react-router-dom and azure/msal-react, acquiring token in react router loader after login redirect fails

I am building out a React application using react-router-dom (v6.4.3) for navigation and azure/msal-react (v18.0.24) for authenticating with our Azure AD tenant. The application is accessible externally but the entire application is protected by authentication (no unprotected routes). Therefore, I have the following setup
if (
!msalInstance.getActiveAccount() &&
msalInstance.getAllAccounts().length > 0
) {
msalInstance.setActiveAccount(msalInstance.getAllAccounts()[0]);
}
msalInstance.enableAccountStorageEvents();
msalInstance.addEventCallback((event: EventMessage) => {
if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
const payload = event.payload as AuthenticationResult;
const account = payload.account;
msalInstance.setActiveAccount(account);
}
});
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<MsalProvider instance={msalInstance}>
<MsalAuthenticationTemplate
interactionType={InteractionType.Redirect}
authenticationRequest={{
scopes,
}}
>
<RouterProvider router={router} />
</MsalAuthenticationTemplate>
</MsalProvider>
);
I'm using react-router-dom loaders to fetch data on route navigation. The endpoints I request data from expect the access token from MSAL in the headers, and to satisfy this I am using an Axios instance configured with an interceptor to retrieve the token from the active account and attach it to headers before sending requests. My router is configured using createBrowserRouter, where a route may look like
export default createBrowserRouter({
// ...excluded for brevity
{
path: "search",
element: <Search />,
loader: ({ request }: Args) {
const url = new URL(request.url);
const t = url.searchParams.get("t") as SearchTypes;
const q = url.searchParams.get("q") as string;
const request = { t, q};
const results = getData(request);
return defer({ results });
}
}
})
My axios interceptor looks something like this.
axiosInstance.interceptors.request.use(
async function (config) {
const token = await getToken();
config = {
headers: {
Authorization: "Bearer " + token,
},
...
}
This all works fine if navigating the app through the "normal" path - going to www.mysite.com, being redirected to Azure AD login, being redirected back to the app index and proceeding to navigate the app. The issue I have is when an unauthenticated user tries to directly access a route, ex. www.mysite.com/search?foo=bar.
When this happens, the user is first directed to the Azure AD login, but after authenticating and being redirected back to www.mysite.com/search, it seems as though the react-router-dom loader is dispatching the API request before the active account has been set in MSAL, as in my getToken method
export default async function getToken() {
const account = msalInstance.getActiveAccount();
if (!account) {
console.error("No active account.");
throw Error("No active account! Verify a user has been signed in.");
}
const response = await msalInstance.acquireTokenSilent({
account,
scopes,
});
return response.accessToken;
}
an error is thrown because account is somehow null, though authentication would have to be satisfied to reach this point. I have tried wrapping my RouterProvider in AuthenticatedTemplate HOC as well, but to no avail, account is still null when getToken is being called. If I refresh the page at this point, the account is no longer null and the token is able to be retrieved from the active account.
Visually, here is what I'm seeing in the console.
If I remove loaders from the equation and fetch my data in the components themselves, it appears to work as expected.
Is there a way to make this work using react-router-dom loaders? I don't understand what issue is causing this behavior. I'd like to continue using the loaders, but if it's not possible I am fine with data fetching in the components.

Correct way to handle JWT tokens in cookies for authentication on the client-side

I have a backend that responds with a JWT token upon successful authentication. However, for security purposes, the cookie can not be visible from the client-side. Now in the frontend I use react. I have created a functional component that sets and gets a token. I have learned that the best approach to store the JWT token is to save it as a cookie as well as activate HTTPOnly from server side. The question I have is for the purpose of securing the JWT token how can I properly save the cookie in the frontend if the HTTPOnly is activated (which makes the cookie not visible)? I plan to use the cookie to check if the user is already logged in or not. Just to give you an overview of what I am doing I have added the component below.
export default function useToken() {
const getToken = () => {
const tokenString = localStorage.getItem("token"); // I will refactor localstorage to cookie
const userToken = JSON.parse(tokenString as string);
return userToken;
}
const [token, setToken] = useState<string>(getToken());
const saveToken = (userToken: string) => {
localStorage.setItem("token", JSON.stringify(userToken)); // I will refactor localstorage to cookie
setToken(userToken)
}
return {
setToken: saveToken,
token
}
}
......
const { token, setToken } = useToken()
return (
<div className="app">
<Header />
<Footer />
<Router>
<Routes>
<Route path="/login" element={!token ? <Login setToken={setToken} /> : <Navigate to="/" />} />
.....
To check if the user is already logged in or not, one common way is :
as soon as one request from the backend returns HTTP 401, the user is considered logged out.
Also, it depends if your frontend is in a CDN or hitting the server on page load. On the latter case, you can find it out server-side.
Then, to handle authentication, your server can read the JWT token from the cookies when receiving the request.

How to redirect an user to a specific webpage after Google Sign In with Redirect in Firebase?

The nature of the sign-in flow with Google/Facebook is that after we login in the redirected page of Google, it comes back to our website's sign-in page.
The following code runs when the Google/Facebook login button is clicked:
fire.auth().signInWithRedirect(provider);
So, my current approach is that I check the Firebase user object using the onAuthStateChanged() function. If the user state is populated, I render a component, else if it is null, I render the component.
{user ? (
<Home />
) : (
<Signup />
)}
But the problem is that after logging in using Google or Facebook, the component is showing for some time (maybe 1-2 secs) and then rendering the component.
I want to render the component immediately after I login using Google redirect. What should I do?
google and facebook login system are asynchronous in nature so you shoud you async await method inside you code.
You should show a full loading state in the login screen when clicking on the signin button
So when the authentication starts show the user a loader,
and if fails stop the loader
you could do something like
if (authUser === undefined || isLoading) {
return <AuthLoader />;
}
return <LoginComponentContents/>
I would recommend to use for all authentication methods the onAuthStateChanged listener in auth. That way it doesn't matter what method you use. It will give you the user if someon is logged in and null if not. The code looks like this:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
} else {
// No user is signed in.
}
});
Use that listener to define the state of your auth (if a user is signed in or not). You can also persiste the data in your state menegement to awoid flickering on page reloads. You can use the same method for email/password login. That way you have a single and simple solution for all Firebasse authentication methods.
For the time you await the Google or other provider redirect login I would recommend to set a loading flag and show a circular loading indicator. The login could always fail. I would not recommend to show pages that require a signed in user untill you get the response from authStateChanged as expected.
For Firebase v9 and above:
If using an AuthContext file, you can define your signInWithRedirect and getRedirectResult functions. Then, import those functions into your Signup page.
AuthContext:
//Log in user with provider. Redirects away from your page.
async function loginGoogle() {
await signInWithRedirect(auth, provider);
return
}
//Checks for provider login result, then navigates
async function getRedirect(){
const result = await getRedirectResult(auth);
if (result) {
navigate('/')
}
}
In your Signup page, call the login function on button click
//Sign in with Google.
async function handleGoogleLogin(e) {
e.preventDefault()
try {
setError('')
await loginGoogle()
} catch(error) {
setError('Failed to log in')
console.log(error)
}
}
Then, just place the getRedirect() function in your Signup component. This function will run when the page reloads from the google redirect, thus sending your user to the desired page.
//Checks for Google Login result.
getRedirect();
For me, it only worked when using this 2-step approach because when the provider redirect occurs, the async function appears to not finished as expected. So loginWithRedirect in one step, then getRedirectResult and navigate in a second step.

Handling authentication persistence in React (with Redux and Firebase)

In my React project, I've implemented authentication using Firebase. The workflow is as follows - The UID which is received upon signing in with Google OAuth is used to query the users in the firestore, and accordingly, the user is fetched. This user is then updated to the redux state by the appropriate handler functions. This is the implementation of the sign-in process. The setUser function does the task of updating the redux state
googleAuth = async () => {
firebase.auth().signInWithPopup(provider).then(async (res) => {
const uid = res.user.uid;
const userRef = firestore.collection('users').where('uid', '==', uid);
const userSnap = await userRef.get();
if(!userSnap.empty) {
const user = userSnap.docs[0].data();
this.props.setUser(user);
}
this.props.history.push('/');
}).catch(err => {
alert(err.message);
})
}
The presence of "currentUser" field in the user state of redux, as a result of the aforementioned setUser function, is the basis of opening the protected routes and displaying relevant user-related details in my project. This currentUser is persisted in the state using Redux Persist, so as long as I don't sign out, the localStorage has the "currentUser" object stored, thus meaning that I am signed in.
This implementation, however, causes a problem, that if I update the localStorage using the console and set the currentUser without actually logging in using my Google credentials, the app recognises me as logged in, and I am able to browse all protected routes. Thus, if someone copies some other user's details from his device's localStorage, then he/she can easily access their account and change/delete important information.
What is the correct way to implement authentication in React then, assuming that I do want login state to persist until the user does not sign out manually

Adding reset password functionality to react / redux login functionality

I used the following login with react/redux tutorial to build a signup / signin functionality into my React app, however I did not realize until recently that I now also need a reset-password / forgot-password functionality.
This feature is not a part of the tutorial at all, and I am simply wondering if anybody has any suggestions as to how I can go about this?
Let me know if I can share any info about my app that will help with this, or if there's a better place to post this type of question. I'm holding off on sharing more on the app as I think it's redundant given the info in the tutorial is nearly exactly how my signup / signin is setup.
Thanks!
After the user enters the proper credentials that you state (usually username, email, or both)
Make an api call to your backend that creates a password reset token. Store it in the database and, in one form or another, associate it with the user (usually it's the same database entry).
Send an email to the user with a link that has the password reset token embedded into it. Have a route in your react-router routes that will handle the url you link to.
Have the route mount a component that has a componentDidMount, which takes the token and makes an api to the backend to validate the token.
Once validated, open a ui element in the react component that allows the user to set a new password
Take the new password, password confirmation, and reset token and make an api call to the backend to change the password.
Delete the reset token in the backend after successful password change
// in your routes file
<Route path="/password_reset/:token" component={PasswordResetValidator}/>
//
class PasswordResetValidator extends React.Component {
state = {password: '', passwordReset: '', isValidated: false}
async componentDidMount() {
const response = await validatePasswordResetToken(this.props.token)
if (response.ok) {
this.setState({ isValidated: true })
} else {
// some error
}
}
handleSubmit = () => {
const { token } = this.props
const { password, passwordReset } = this.state
sendPasswordResetData({password, passwordReset, token})
// probably want some confirmation feedback to the user after successful or failed attempt
}
render() {
if(this.state.isValidated) {
return (
<div>
<input />
<input />
<button onClick={this.handleSubmit}>Set new password</button>
<div>
)
}
return // something while token is being validated
}
}
Obviously you need to make your own text input handlers. You should also have error handling, and good ui feedback to the user. But ultimately, that's all at your discretion.
Best of luck

Resources