Skip to main content
All Posts
2026The Architect's Guide to React Authentication: Choosing a Flow, a Store and a BoundaryReact Authentication Best Practices: The 2026 ChecklistBest Authentication Libraries for React in 2026Open Source and Self-Hosted Auth for React in 2026JWT-Based Authentication in React: Storage, Validation and the localStorage QuestionPasswordless Login in React: Passkeys, Magic Links and OTPOAuth 2.0 and OIDC in React: The Authorization Code Flow with PKCE, ExplainedReact Authentication: The Complete Guide

The Architect's Guide to React Authentication: Choosing a Flow, a Store and a Boundary

· 32 min read
Sagara Gunathunga
Head of DevRel - IAM

You may be asked to add login and user sign-up to your React application.

It sounds like an implementation task, but before you choose an SDK or write any code, there are a few architecture decisions you need to get right. Where should identity live? Who should be responsible for it? What should reach the browser? How should your React application connect to it? And when the application calls a backend, where should access actually be enforced?

This guide is intended to help you think through those questions as an architect.

It does not assume that you already understand identity standards, protocols or IAM terminology, and the goal is not to teach you all of them. We will introduce those concepts only when they help explain a particular architecture decision.

Instead, the focus is on giving you the right questions to ask yourself and the other teams involved, so you can find the right answers for your application and establish a solid authentication architecture before the implementation starts.

We will work through the architecture by answering a few questions: what does identity actually mean for a React application, and why does part of it need to live outside the browser; who should run and own that identity capability; once a user signs in, who should be responsible for handling the tokens; how should your React application connect to the identity system; what identity information can React use inside the application; and when React calls a protected backend, where should the real access-control decision happen.

By the end, you should have a clear picture of the authentication architecture around your React application and know what decisions need to be made before choosing a product, SDK or implementation pattern.

Start with what "identity" means for your application​

When somebody says "we need authentication," the visible requirement is normally sign-in.

But a real application usually needs more than checking a username and password.

It needs to know who the user is. It needs some way to decide what that user can do. It needs to remember that the user already signed in so they are not asked to prove themselves on every API call. Somebody has to create, update and disable user accounts. Eventually there will be password recovery, multi-factor authentication, enterprise sign-in or another requirement that was not part of the first implementation.

These different jobs have names in the identity world.

Checking who someone is is authentication.

Deciding what they are allowed to do is authorization.

Creating, updating and disabling their accounts is generally part of user lifecycle management.

The terminology itself is less important than understanding that these are separate responsibilities.

A user being successfully authenticated does not mean they should be allowed to do everything in your application.

As an example, both a finance administrator and a normal employee may successfully sign in. Authentication tells you that both users are who they claim to be. Authorization decides whether each user can approve a payment.

The important question for React architecture is where these responsibilities can safely live.

Why identity cannot simply live inside React​

A React application runs inside a browser on a device you do not control.

Anyone using your application can download the JavaScript, inspect it, debug it and change what runs in their browser. Anything included in the bundle should therefore be treated as public.

That gives us a useful architectural boundary.

React can participate in authentication, but it cannot be the trusted authority for authentication.

Consider a simple example:

{user.isAdmin && <AdminPanel />}

There is nothing wrong with this.

If the user is not an administrator, hiding the admin interface gives them the correct user experience.

But this code cannot protect the backend operation itself.

A user can modify the JavaScript. They can skip the UI completely and call your API directly.

So the React application can use identity information to decide what to display, but the trusted access decision still needs to happen somewhere outside the browser.

The same applies to sign-in itself.

React can collect a password, redirect the user to a sign-in page or start a passkey flow. But something outside React needs to keep the user record and verify the proof.

Account recovery also needs something outside the browser. The system may need to send an email, validate a one-time link, record recovery attempts and invalidate old credentials.

Once you look at the problem this way, identity is not another library that belongs inside your component tree.

It is a capability your application depends on.

The next architectural question is therefore straightforward:

Where should that identity capability run, and who should be responsible for it?

Four ways to provide identity​

There are four common answers.

You can build it yourself, use an authentication library inside your application, run a separate identity server, or use a managed identity service.

Each can produce a working sign-in screen.

The differences normally become visible later, when the requirements grow.

Build it yourself​

The first option is to own the identity implementation yourself.

Your backend stores users, verifies passwords and creates whatever session mechanism your application uses.

For a prototype, a hackathon or a small internal application, this can be reasonable.

The problem starts when the requirement stops being simply "check this password."

Soon you need email verification, password recovery, account locking, session revocation, multi-factor authentication, audit information and possibly single sign-on with another company's identity system.

None of these is impossible to build.

The real question is whether identity is something your application team wants to build and continue operating several years from now.

You are also taking responsibility for the security of that system. Libraries need security updates, behaviour has to be reviewed, and every new requirement extends a piece of infrastructure that your team created mainly because the application originally needed a login screen.

For most production applications, I would only choose this when owning identity itself is an intentional requirement.

Use an authentication library inside the application​

A second option is to use an authentication library such as Auth.js or Better Auth.

This removes a lot of low-level implementation work.

Instead of writing password handling, sessions and provider integrations completely yourself, you use a library that already provides much of that behaviour.

The important architectural point is where it runs.

The authentication capability still lives as part of your application stack.

For one application, this can be a very practical model.

But assume you later have three applications that should share the same users. Or an enterprise customer asks to connect its own company login. Or security wants authentication policy managed in one place.

At that stage, identity is starting to become reusable infrastructure rather than one feature inside one application.

That does not make an authentication library the wrong choice. It tells you where the natural boundary of that approach sits.

Run a separate identity service​

The third option is to move identity into its own service.

Examples include WSO2 Identity Server from WSO2 Identity Platform and Keycloak.

Your React application no longer owns the full authentication lifecycle. Instead, it redirects the user to the identity server, or still captures user input and calls an API exposed by the identity service which performs authentication and returns the result.

This separation becomes useful when multiple applications need the same users or security policies.

React remains responsible for your application experience. The identity service becomes responsible for identity.

The trade-off is operational.

You now have another production service to run. Your team is responsible for upgrades, security patches, availability, backups and configuration.

For organizations that need identity inside their own infrastructure, that control may be exactly what they want.

For teams that do not want another platform to operate, it may not be.

Use identity as a managed service​

The fourth option keeps identity as a separate capability but lets somebody else operate it.

Examples include WSO2 Identity Platform and Auth0.

The provider takes care of running the identity infrastructure, including upgrades, scaling and availability. Your team integrates the application and configures the authentication behaviour it needs.

For many teams, this is the shortest route to production authentication because identity does not become another service they need to operate.

The trade-off moves somewhere else.

You depend on an external provider, its deployment model and its commercial model.

If identity must remain inside your infrastructure, if the environment has no route to the public internet, or if you have a hard data-residency requirement that the service cannot meet, a managed public service may not fit.

The four approaches can be summarized as follows.

ApproachWhere identity runsWho operates itNatural fit
Build it yourselfYour application backendYour teamSpecialised cases where you deliberately want to own identity
Authentication libraryYour application stackYour teamSmall application-centric authentication
Separate identity serverIndependent serviceYour teamIdentity shared across applications, with infrastructure under your control
Managed identity serviceIndependent serviceProviderShared identity without operating the identity platform yourself

Once you make this decision, the user can sign in.

But your React application still has another problem.

The identity system knows that the user successfully authenticated. Your application and APIs now need a way to carry that result across requests.

This is where tokens enter the architecture.

Why tokens exist​

Assume the user signs in successfully.

You do not want to ask them to prove who they are again every time React loads a page or calls a backend. Instead, the identity system returns information that represents the result of that authentication. This information is commonly carried using a token.

There are several token types depending on the protocol and purpose. OAuth 2.0 has access tokens, OpenID Connect has ID tokens, and SAML uses assertions. Their formats can also be different. A token may be represented as a JWT, while a SAML assertion is XML, for example.

For this part of the architecture, the important distinction is between the OpenID Connect ID token and the OAuth 2.0 access token.

When your application needs to establish who the signed-in user is, the token designed for that purpose is the ID token. OpenID Connect returns an ID token containing claims about the authentication event and the authenticated user.

A typical ID token may look like this after it is decoded:

{
"iss": "https://id.example.com",
"sub": "248289761001",
"aud": "react-app",
"exp": 1787817600,
"iat": 1787814000,
"name": "Alice",
"email": "[email protected]"
}

You do not need to understand every possible claim at this stage. A few are enough to understand how the token is used: iss tells you which identity system issued the token, sub is the stable identifier for the authenticated user, aud tells you which application the token was issued for, exp tells you when the token expires, and claims such as name and email provide information about the user.

The important part is that your application should not simply decode this JSON and trust what it says.

The ID token needs to be validated first. At a minimum, the application handling the OpenID Connect flow needs to verify that the token came from the identity provider it trusts, was issued for the expected application, has not expired, and has a valid signature.

In practice, you normally do not write this validation logic yourself. If your React application handles the OpenID Connect flow directly, the SDK should perform these checks for you. If you use a backend such as a BFF, that backend validates the ID token, establishes the application session and gives React only the user information it needs.

An access token has a different job.

It is issued so a client can access a protected API. The API, rather than the React application, is the intended consumer of that token.

This distinction is easy to miss because an ID token and an access token may both use the JWT format and may even contain some similar claims. That does not make them interchangeable.

If your application needs to know who signed in, that identity should come from the OpenID Connect authentication result and the ID token, or from an application session established from that result. You should not take an OAuth access token, decode it and treat whatever user information happens to be inside it as the application's authenticated identity.

The same principle applies regardless of who handles the tokens.

If React handles the OpenID Connect flow directly, it receives and validates the ID token through the SDK.

If a BFF handles authentication, React may never see the ID token at all. The BFF validates it, creates an application session and exposes the authenticated user state to React.

The access token remains for a different boundary: calling the protected API.

This is why tokens appear in the architecture in the first place.

Your React application runs in one place, the trusted identity system runs somewhere else, and the protected API may run somewhere else again. Tokens allow authentication results and API access to move across those boundaries without repeating the sign-in process for every request.

In modern React applications, the standards you will most commonly encounter here are OAuth 2.0 and OpenID Connect, usually shortened to OIDC.

You do not need to implement these standards yourself to use them. Most identity platforms and SDKs handle the protocol details.

But you do need to make one architecture decision that directly affects your application:

Who is responsible for handling these tokens?

There are three common patterns.

React handles the tokens​

The first pattern is common in single-page applications.

In this model, the React application participates directly in the OpenID Connect flow. After the user signs in, the application receives the authentication result and the tokens it needs.

That normally means two different things are happening: the ID token is used to establish the authenticated user, and the access token is used when React calls a protected API.

If the identity provider also issues a refresh token to the browser application, that credential can be used to obtain new access tokens without asking the user to sign in again.

The important architectural point is that React is now directly responsible for handling some or all of these credentials.

When React calls an API, the access token is normally sent as follows:

Authorization: Bearer <access-token>

As discussed earlier, the access token is for the API. React should not decode that token and use whatever user information happens to be inside it as the source of truth for the signed-in user.

The authenticated user should come from the validated OpenID Connect authentication result, normally the ID token and the user state built from it.

Why PKCE appears in this pattern​

A React application runs in the browser, so it cannot safely keep a permanent client secret.

That creates a problem during the authorization flow. The application needs to exchange the authorization code returned after sign-in, but it cannot authenticate itself using a secret embedded in the JavaScript bundle.

This is where Proof Key for Code Exchange, or PKCE, comes in.

When React starts the sign-in flow, it creates a temporary random value and sends a derived value with the authorization request. When the authorization code comes back, the application has to provide the original value before the identity provider will complete the exchange.

In simple terms, PKCE helps prove that the application completing the flow is the same one that started it.

Your React code normally does not implement this directly. A suitable OIDC SDK handles it for you.

PKCE protects the authorization-code exchange. It does not make the browser itself trusted, and it does not solve the problem of protecting tokens after they have reached the browser.

That leads to the next decision.

Where do the tokens live?​

Once tokens are available to the React application, they have to be kept somewhere for as long as the application needs them.

There is no browser storage option that turns an untrusted browser into a trusted environment. The choices mainly change how long the credential remains available and how easily it can be extracted.

Storage locationSurvives refreshMain consideration
MemoryNoToken disappears when the page is reloaded
Web WorkerNoCan isolate token handling from normal application code, with additional complexity
sessionStorageYes, within the tabJavaScript running on the page can read it
localStorageYesJavaScript running on the page can read and copy it

The distinction that matters here is between stealing a credential and using the authenticated session while malicious code is already running inside the page.

Keeping an access token in memory can reduce the chance that an attacker extracts it and reuses it later from somewhere else.

But if malicious JavaScript is already executing in your application, it may still be able to make API requests as the user while that page remains active.

The same consideration applies to the ID token. React may need the authenticated user state derived from it, but that does not mean the application should keep the raw ID token around longer than necessary.

A good SDK can hide much of this lifecycle from your application code, but the architectural boundary does not change: in this pattern, authentication credentials reach the browser, so browser compromise remains part of your threat model.

Your backend handles part of the token flow​

The second pattern assumes your React application already has a backend.

The user still signs in with the identity provider, but the backend takes responsibility for the final part of the OpenID Connect flow.

Instead of React exchanging the authorization code directly, the backend performs that exchange and receives the tokens.

At this point, the backend can validate the ID token, establish who the user is, and keep longer-lived credentials such as the refresh token on the server.

React can still receive a short-lived access token if it needs to call a protected API directly.

So the responsibilities are split: the backend handles the OpenID Connect response and establishes the authenticated user, the backend keeps the refresh token, and React may still receive a short-lived access token for API calls.

This is an important distinction from the first pattern.

React no longer needs to treat the ID token as part of its authentication state, and it does not need to keep the refresh token. The backend can create an application session or return only the user information React actually needs.

The access token is still different. If React needs to call the protected API directly, the backend can give it a short-lived access token for that purpose.

The flow now looks more like this:

React application
|
| Start sign-in
v
Identity provider
|
| Authorization code
v
Backend
|
| Token exchange
v
Identity provider

Backend
| \
| \ Refresh token stays here
|
| Short-lived access token
v
React application
|
| Access token
v
Protected API

This changes the exposure considerably.

The refresh token is normally the credential with the longest useful lifetime. If it remains on the server, compromising the React application does not automatically give an attacker the ability to continue obtaining new access tokens over a long period.

React still handles an access token, so the browser has not disappeared from the credential path completely.

If malicious JavaScript runs in the page, it may still be able to use or extract that access token while it is valid.

The difference is that the damage is bounded by the lifetime and privileges of that short-lived token rather than by a longer-lived refresh credential.

This pattern is therefore useful when you already have a backend and want to move the most sensitive parts of the authentication flow out of the browser without changing the way React calls your APIs.

The backend becomes responsible for authentication and token renewal.

React remains responsible for presenting the authenticated experience and using short-lived access tokens when it needs to call protected APIs directly.

If you want to remove access tokens from React as well, the boundary needs to move one step further.

Your backend handles all tokens​

The third pattern moves token handling completely out of the React application.

React never receives the ID token, access token or refresh token.

The backend handles the OpenID Connect flow, validates the ID token, establishes the authenticated user and keeps all OAuth tokens on the server.

React receives an application session instead.

This is normally represented using a secure, HTTP-only cookie.

From React's point of view, authentication now looks much simpler.

The browser does not need to know anything about OAuth tokens. It only knows that it has an authenticated application session.

When React needs data, it calls your backend.

The browser sends the session cookie automatically, the backend identifies the user, obtains or uses the appropriate access token, and calls the protected API on the user's behalf.

This architecture is commonly called a Backend for Frontend, or BFF.

The flow looks like this:

React application
|
| Start sign-in
v
BFF
|
v
Identity provider
|
| Authorization response
v
BFF
|
| Validate ID token
| Keep access + refresh tokens
| Create application session
v
React application
|
| Session cookie
v
BFF
|
| Access token
v
Protected API

The important boundary is now very clear.

React never needs to handle identity tokens or API tokens directly.

The BFF establishes the user from the OpenID Connect response and exposes only the user information the application needs, for example:

{
"id": "248289761001",
"name": "Alice",
"email": "[email protected]"
}

React can use this information to render the application, but the raw ID token does not need to leave the backend.

The same applies to access tokens.

When React needs to call a protected API, it calls the BFF. The BFF attaches the access token and makes the downstream request.

This removes token theft from React code as a direct concern because there is no OAuth token in the JavaScript environment to steal.

But it does not make the browser trusted.

If malicious JavaScript runs inside the page, it may still make requests to the BFF while the user's session is active. The session cookie can be sent automatically with those requests even though JavaScript cannot read it.

So the BFF changes the problem rather than eliminating it.

You still need to protect against script injection, and because authentication now relies on cookies, you also need to consider cross-site request forgery protections, cookie attributes such as HttpOnly, Secure and SameSite, and the lifetime of the application session.

There is also an architectural cost.

The BFF is now in the path of every protected request.

That means it becomes part of your application's availability, latency and scaling requirements. You also need somewhere to maintain the session and token state associated with each user.

So I would not think of the BFF as simply the "most secure" of the three patterns.

It is a different boundary.

You are moving authentication state and OAuth credentials out of the browser and accepting more responsibility on the backend.

For applications handling sensitive data, or where you already have a backend that naturally sits between React and the protected APIs, that can be a very useful trade.

At this point, the three patterns can be viewed as one progression:

Pattern 1
React handles ID/access/possibly refresh tokens
↓ move long-lived credentials out
Pattern 2
Backend handles authentication + refresh token
React handles short-lived access token
↓ move all OAuth tokens out
Pattern 3
Backend handles all tokens
React handles only an application session

The question is not which pattern is universally correct.

The question is how much credential handling you want inside the browser, and how much backend responsibility you are willing to introduce to move it out.

How React connects to identity​

There are several levels at which you can integrate authentication into a React application.

I find it useful to think about these as three layers.

At the lowest level, your application understands most of the underlying protocol.

At the next level, an SDK handles the protocol and gives your React code authentication APIs.

At the highest level, the SDK also gives you ready-made React components.

You may see these referred to as L1, L2 and L3 SDKs.

The terminology is less important than the architectural question:

How much authentication-specific behaviour do you want inside your application code?

L1: work close to the protocol​

At this level you use a general OAuth or OpenID Connect client library and manage much of the application integration yourself.

You configure the authorization flow, process redirects, handle tokens, deal with expiry and refresh behaviour, and manage sign-out.

The advantage is control.

Your application is normally less coupled to one identity provider because it works directly against standard protocol behaviour.

The cost is that your application team now owns more identity-specific code.

I would use this level when protocol-level control or provider portability is an actual requirement, or when no suitable higher-level SDK exists.

I would not choose it simply because fewer abstractions look cleaner on an architecture diagram.

L2: use a headless SDK​

At the next level, the SDK handles the protocol and exposes APIs that fit naturally into React.

For example:

const { user, signIn, signOut } = useAuth();

The SDK takes responsibility for things such as redirects, token lifecycle and authentication state.

Your application still owns the interface.

You decide how the sign-in button looks, how errors are shown, what loading looks like and how user information fits into the rest of your design system.

This is often a good balance for React applications where authentication should behave like part of the product rather than a separate UI.

L3: use higher-level React components​

At the highest level, the SDK provides the React integration as well as ready-made components.

You may get components for sign-in, sign-out, user profiles and other common authentication experiences.

This reduces the amount of application code you need to write.

The trade-off is coupling.

Your JSX now contains components and APIs from a particular SDK. If you later change identity providers, that integration becomes part of the migration.

That may still be the right trade.

If the component gives your team a production-ready sign-in experience quickly and you have no strong requirement for provider portability, avoiding unnecessary authentication code can be more valuable than optimizing for a migration that may never happen.

The three levels look like this.

Integration levelProtocol handlingUI handlingProvider coupling
L1: protocol-levelMostly your applicationYour applicationLowest
L2: headless SDKSDKYour applicationMedium
L3: component SDKSDKMostly SDKHighest

There is another part of this decision I would not ignore: who maintains the SDK.

A provider-maintained SDK is normally the most closely aligned with that identity platform and may support new provider capabilities first.

A community-maintained SDK can provide more independence from any one provider, but you should look carefully at maintenance activity, release frequency and the number of people actively supporting it.

A standards-oriented library may offer the least provider coupling, but you normally work closer to OAuth and OIDC concepts.

This is why choosing an SDK is an architectural decision rather than simply a package-selection decision.

Now React knows who the user is.

The next question is what your application should actually do with that information.

Using identity inside the React application​

After authentication, your React application normally has some information about the user.

Depending on your setup, this may come from an ID token, a user-info endpoint, a session API or your own backend.

The information may include things such as a user identifier, name and email, roles, groups, scopes or permissions, and organization or tenant information.

React can use this information to shape the application experience.

As an example, you may only show an administration menu when the user has the appropriate role:

{user.roles.includes("admin") && <AdminMenu />}

Or you might enable a particular operation based on a permission:

{user.permissions.includes("invoice:approve") && (
<ApproveInvoiceButton />
)}

This is where identity becomes useful inside the application rather than simply being something that happened during sign-in.

But there is an important boundary.

A frontend access check is a user-experience decision, not the final security decision.

If React hides a button, that prevents the normal user from clicking the button.

It does not prevent somebody from calling the corresponding API themselves.

The browser is still under the user's control.

So frontend authorization can help you present the correct interface, but every sensitive operation still needs to be checked again by the backend.

This is the point where the two identity concepts we introduced earlier become useful.

Authentication answered: who is this user?

Authorization now answers: is this user allowed to perform this operation?

React can participate in authorization by shaping the UI.

The backend must enforce it.

That leads us to the final part of the architecture.

Calling a protected backend​

Once the user is signed in, your React application normally needs to call one or more protected APIs.

This is where the OAuth 2.0 access token comes into play.

As we discussed earlier, the ID token is used to establish the authenticated user. The access token has a different purpose: it is the credential presented to a protected API.

If React handles access tokens directly, the request normally looks like this:

Authorization: Bearer <access-token>

The API should treat that token as a credential, not simply as a piece of JSON containing user information.

Before doing anything with the request, the API needs to validate the access token. Depending on the token type and the identity system, that may involve checking its signature or using token introspection.

At a minimum, the API needs to establish that the token came from an issuer it trusts, has not expired, and was issued for that API.

Once the token is accepted, the API can use the information associated with it to make an access-control decision.

As an example, assume your React application shows an Approve invoice button because the authenticated user has the appropriate permission.

The frontend check gives the user the correct experience:

{user.permissions.includes("invoice:approve") && (
<ApproveInvoiceButton />
)}

When the user clicks that button, React sends a request to the protected API using the access token.

The API must still make its own decision about whether that access token allows the operation.

That decision may be based on scopes, roles, permissions or application-specific policy. The exact model can vary, but the architectural rule does not:

React can use identity information to decide what to show. The protected API must use the access token to decide what the caller is allowed to do.

This is also why you should not rely on the ID token when calling an API.

The ID token was issued to the client application so it can establish the user's authenticated identity. The access token was issued for access to a protected resource.

Even if both happen to be JWTs and contain similar-looking claims, they have different purposes and different intended audiences.

What changes with the three token-handling patterns?​

The access token still exists in all three architectures. What changes is who handles it.

If React handles the tokens directly:

React application
|
| Access token
v
Protected API

React obtains the access token and sends it with the API request.

If your backend handles part of the token flow:

Backend
|
| Short-lived access token
v
React application
|
| Access token
v
Protected API

The backend keeps the refresh token and gives React only the short-lived access token it needs for API calls.

With a BFF:

React application
|
| Application session
v
BFF
|
| Access token
v
Protected API

React never receives the access token. It calls the BFF using its application session, and the BFF presents the access token to the protected API.

This is an important point because the BFF does not remove OAuth access tokens from the architecture. It moves them away from the browser.

The API is the final enforcement point​

Regardless of which of these patterns you choose, the protected API must enforce access independently from React.

A route guard such as this:

<Route
path="/admin"
element={user.isAdmin ? <Admin /> : <Navigate to="/" />}
/>

is useful for controlling what the user sees.

It is not an access-control boundary.

Someone can bypass the React application and call the API directly.

So the API must validate the access token and make the same access decision before performing the protected operation.

This gives us a clean separation of responsibilities:

ID token
|
| Establish who signed in
v
Client application / BFF

Access token
|
| Request access to a protected resource
v
Protected API
|
| Validate token + enforce access
v
Resource

The distinction between these two tokens is one of the most useful things to keep clear in the overall architecture.

The ID token establishes the authenticated identity for the client.

The access token carries the authority needed to call the protected API.

Once those two responsibilities are separated, it becomes much easier to reason about what React should know, what the backend should know, and where the final access decision belongs.

Putting the architecture together​

At this stage, the pieces connect.

You started with a React application that needed sign-in.

First, you established that identity is larger than a login screen and that the trusted part of it cannot live inside the browser.

Then you decided where identity should run: inside your own implementation, through an application authentication library, as a separate self-hosted identity server, or as a managed identity service.

Once the user authenticated, you decided who should handle the tokens: React, React plus your backend, or your backend completely, using the BFF pattern.

Then you decided how much of the identity integration should appear inside your React code: L1 protocol-level integration, L2 headless SDK, or L3 higher-level React components.

Finally, React used the authenticated identity to shape the application experience, while the backend remained responsible for enforcing the real access-control decision.

That gives you the complete architecture:

                +----------------------+
| Identity System |
+----------+-----------+
|
Authentication
|
v
+--------+ +---------+----------+
| User | -------> | React Application |
+--------+ +---------+----------+
|
Session / Token
|
v
+---------+---------+
| Protected Backend |
+---------+---------+
|
Authorization
|
v
Resources

There are several products, SDKs and protocol options behind each box, but I would not start by choosing those.

Start by deciding the boundaries.

Who owns identity? Who handles the token? How much identity logic belongs in React? And where is access actually enforced?

Once those decisions are clear, evaluating a particular identity platform or React SDK becomes much easier because you are evaluating it against an architecture rather than allowing the product to define the architecture for you.

Choosing the identity platform after the architecture​

Only now does provider selection become a useful question.

You are no longer asking which identity platform is "best for React."

You can ask whether it supports the architecture you already decided.

A few things become particularly useful to check.

Does it support standard protocols?​

OAuth 2.0 and OpenID Connect matter because they create a standard boundary between your application and the identity system.

A standards-based architecture does not make provider migration free, but it reduces how much of the integration needs to be proprietary.

Where certification is important to your requirements, you can also verify whether the provider has relevant OpenID certifications rather than relying only on compatibility claims.

Can it run where you need it?​

Some teams want a public managed service.

Others require self-hosting, private cloud or environments without public internet access.

This is normally a much more important architectural requirement than a small difference in SDK APIs.

Does it match your application model?​

If you build software for businesses, you may need organizations or tenants, organization administrators, enterprise federation and different authentication policies for different customers.

You can build that model yourself, but if it is fundamental to the product it is useful when the identity platform already supports it.

Can you extend it?​

Eventually you may need to call an external service during sign-in, add an additional verification step or change the information placed into a token.

The question is not whether the platform supports every future requirement today.

The more useful question is whether it gives you extension points when your requirement falls outside its default behaviour.

What happens if you need to move?​

Provider lock-in is not only about APIs.

Can you export users? What happens to password hashes? Can multi-factor authentication registrations move? What about passkeys? Can the old and new providers run in parallel during migration? How much of your React application depends on provider-specific SDKs or components?

The architecture decisions earlier in this guide determine how difficult most of those answers become.

WSO2 Identity Platform is worth evaluating in this context because it supports both managed and self-managed deployment models, including cases where identity needs to remain under your infrastructure control. It also supports organization-based B2B identity models and provides extension points for changing authentication behaviour without moving that logic back into the React application.

Whether those capabilities matter depends on the architecture you are building.

That is why the provider decision belongs at the end of this process rather than at the beginning.

Where this leaves your React application​

You may have started with a fairly ordinary requirement: add authentication to a React application.

But if you are responsible for the application architecture, the real job is deciding what React should own and what it should not.

Something outside the browser needs to own a trusted identity. You need to decide where tokens are handled and how much of that responsibility belongs in React. Your application then needs a deliberate integration boundary with the identity system, while the backend remains responsible for enforcing access to protected resources.

These decisions are much easier to make before the first authentication SDK becomes part of the application.

Once the boundaries are clear, the implementation becomes a much smaller problem.

WSO2 Identity PlatformWSO2 Identity Platform

The extensible, API-first platform to orchestrate identity for every human and AI agent at scale. 100% open source.

Explore

BlogTutorialsTopics
© WSO2 LLC. All rights reserved.
WSO2 LegalDo Not Sell My Personal InformationModern Slavery Statement