React Authentication why & how

By the end of this post, you will know how to use authentication in your project using React.

I can't imagine anything in the tech world that would sound so similar and frustrates when you are trying to find the difference but Authentication and Authorization. As Front End developers we are facing these terms quite often, mostly to build system using it.

But how exactly we can develop such a thing? What is the real difference? Let's start by defining how these terms differ from each other to understand how we could implement it in our app.

/^Auth(entic|oriz)ation$/#

$1#

Authentication is where your meaningful application usually starts and ends. It answers the question "Are you able to use this app or not?" For all kinds of applications, you'll most likely need that feature for various reasons. These things we are facing daily - Login/Password, 2FA, Certificates, Biometric like FaceID, TouchID, Physical tokens, OpenID - those are all approaches of Authentication.

$2#

On the other hand, Authorization is the next step which answers a bit different question - "What can I do with your app?" Usual approach to get authorization is RBAC - Role-Based Access Control. As for extra examples of access control systems that can be used - ACL or ABAC. Though RBAC is defined as the go-to solution for most of the cases.

It's not that important what kind of application you are building. Usual user interaction with your app will go as follows:

  1. User Authenticates - it lets you in if you are allowed

  2. User assigned an Authorization - it lets you use features

Authentication interface#

Let's start with how we define Authentication. Interface for authentication is consist of 2 basics:

  • unique identifier

  • service that serves as a function "(uniqueIdentifier) => user"

Unique Identifier#

Any approach to authentication gives you a way to generate an identifier that could be used in the system to process further requests. Any key that you have configured for authentication - login and password, 2FA time-based code, face or fingerprint used to generate the identifier.

(unique identifier) => user#

A service on your back end to recognize already logged in users. This function is in control of all user app activity. It handles how we are saving identifiers within our app, how long we are allowing users to use an app without religion.

Here is how we can define Login flow and Authentication flow


Ready to use providers#

When building an authentication system the most challenging part is on the back end. You need to provide scalable service to work as a source of truth for all of your users. Building your own solution with default criteria like "I want to let users in by their email and password" or "I want also to let them in with 2FA for security reasons" most likely would just add extra service to increase your technical debt. Discuss with your team what would be more suitable for your product.

Sometimes you don't need to build it on your own as you can use something from existing services.

There are good production-ready providers that could save your time:

They usually come with different identification strategies already built-in. You can have social integration in 2 clicks. Some of them would also provide front end components that you can use or extend.

React#

Now we have Back End for our authentication following the process described above, it returns our identified user. We are ready to proceed with cool looking front end built with the currently popular framework.

To do that with examples, we are going to use React + MobX + TypeScript on Front End and Express on Back End. If you are not really familiar with typed JS or observables, I would really recommend to check out these articles:

React is the most popular library to build your front end at the moment. But there is no specific answer in the article as to why you should use exactly React, all approaches we are going to look at could be used with Vue, Angular or vanilla JS.

We will use latest feature from React like hooks. If you haven't used them by now check this doc to get familiar with them.

We are just about to build our web app and let all of our components to know about the user. Our front end work here is to:

  1. Put a request to identify the user as fast as we can

  2. Let all components know when we identify a user

  3. If a user is not defined redirect to Log-in page

  4. Let your user a way to log out

All of code examples could be found in the repository here.

fetch(user)#

Our web app may have more dynamic requests for data or view but what we want to do here is put our request in parallel, so our user will be defined as soon as possible. To do that we have 2 ways when to trigger user load:

  1. Before you render - a good case for a static amount of request like global user

  2. While you render - a good case for dynamic loading when you are not sure how much data entities you will have.

As we most likely will have 1 user authenticated and based on that info we would proceed with further app logic, the first scenario is better for us. That's why we putting it in the place where we render the component.

Let's start with defining function where we sending the request to our API to get the user.

src/user/loadUserByToken.ts

Here we are using axios as a library to fetch data, but pretty the same could be done with regular fetch request with defining a bit more info.

As a mock for our user info, we are using service reqres.in. It just returns user info on specific IDs.

We are loading user info by userId for demo purposes but in a real app, you will not have an id before you render. You will have your identifier stored in cookies, localStorage, sessionStorage on the user side. We need that to let our system know on user return and not ask a user to log in each time they visit our app. In the end, this identification will serve the same idea - ask API for authentication by identifier.

src/user/userStore.ts

Here is how we triggering this static request right before rendering app:

src/index.tsx

then#

To serve the global state that should be accessible outside of component we cannot use only context and hooks so we will define a MobX store to follow our authentication process. But on another hand, we can have a case with the need to serve data only in component, for that we will use local state with hooks.

Let's do examples on how saving could be done using both approaches.

  • MobX

Define your User store, what data would you use in your app. Observables let us create simple objects without the need to create all actions around them, this approach is quite straightforward and best for demo purposes.

src/user/userStore.ts

After defining your store you need just to use it inside a component where you expect reading user data. To do that you need to get the user from this store, with injecting or importing directly - depends on your architecture.

src/user/ExampleUserStore.tsx

As an alternative approach to direct import, you could think about putting a wrapper around this store like useUserStore and inject your global user wherever you need it.

  • Context/Hooks to save user per component instead of shared

src/user/useUser.ts

Once we are about to render a component with the hook useUser it will create a local state with a user loaded if not found. As we may use this hook multiple times in different components we need to make sure we are not requesting redundant data when component not rendering anymore. That's why we defined cancel callback on useEffect return.

Then in our actual component, we just using this as regular hook.

src/user/ExampleUseUser.tsx

catch#

If the user is not defined and we cannot proceed with the positive flow - we need to provide a fallback to our login page. It could be just a history replacement or usage of a provider like react-router.

Within our simple page let's say instead of going to the different page we just want to replace our UI to show the input to say what user we are.

src/user/Login.tsx

This small component lets us define a wrapper around other components that rely on userStore data. In case our default userId is wrong we will get fallback to this component with canceling other requests if they are still in progress.

src/App.tsx

And this is how we are wrapping components that rely on data. Here we are assuming that all of the UI relies on the user to be logged in, and once we are putting correct keys - the full data will come up.

finally#

At some point your users may want to log out from your service, so don't forget to let them. A simple function that removes you from the service would work just fine.

We already defined function setUndefinedUser all we need to let some button trigger it.

Let's have this button right under user details.

Login/signup form#

Login and signup forms are usually the first screens your users faced if the user is not found. You may have them manually crafted or generated by some package like aws. The logic for these screens would be as follows:

Login flow

  • Let user input keys (login/password)

  • Send a request to API

    • Encrypt Keys to Identifier

    • Check for user

    • Add Identifier in the system if we are good

    • Persist Identifier on Client-side

As you can see these forms are sharing some logic with our Authentication. Full implementation for them would deserve a separate post so we wouldn't describe them fully here.

Auth/!Auth page view#

When building a public application you may have a lot of functionality shared between random users accessing your app or logged in user who is looking for the same page. Let's build an example of a component that has a difference.

This component would have a condition on what to show depending on the state. You could split them into different pure components or keep in the one, absolutely up to your decision but overall it would be similar to the next component:

src/user/ExampleExtraInfoWhenLoggedIn.tsx

This component would render only the first name if we are logged in. After login, we will see where this person is working.

Security#

There are no specific cases to have extra security problems with React. All of the restrictions touching WEB and browser connection to a server as - "You should prefer secure protocol" and "Don't include viable secrets within your FE code".

HTTPS

As https is pretty a standard over the internet it shouldn't be too hard to migrate your current API to secure version. Secrets on another hand still could be found in dark edges of your FE app.

Encryption

Some authentication could be implemented with thinking - It is harmful to pass bare passwords to back end and validate there. This approach leads to implementing encryption on FE and opens your app to pass the hash attack. Feel free to pass passwords barely over https, it will do all the work for you.

Logging

In your FE app, you may have logging included so you can monitor all critical events to your system after some time. How is your system used, what is the user behavior? And at some point, you may have leaking personal data to your logging system, especially for passwords make sure you are not passing it anywhere. And this not comes to passwords, but to any personal data, you may store for users.

Summary

  • HTTPS used

  • no secrets are hardcoded to a web app

  • personal info removed out from logging

Up-to-date login information#

Growing your application you might want to make sure your users are getting relevant page/information each second of usage. Things like changing your account in any way that changes the behavior of your app.

We can think of situations like:

  • account changing (email/password)

  • role changing

  • business cases like device linking

Fortunately, we have technologies like Server Side Events, WebSockets that we can use to sync all of our UIs to a state on the back end. With HTTP/2 technology there is no need to have a full WS server to be in sync with the back end.

Let's consider our internal API to have next endpoints:

  • login

  • logout

  • /eventstream/:userId

With these features, we are simply implementing a pubsub approach where we add a topic on login, remove the topic on logout and subscribing on eventstream. Once we see our user logged out from different tab we can send a message to all registered tabs saying they are not valid anymore.

api/index.js