Integrate authentication into React + API
This guide shows how to create a simple React application and secure it with authentication powered by Ory. The guide provides the setup for using Ory Network, but the code can be used with both Ory Network and self-hosted Ory software.
This guide is perfect for you if:
- You have React installed.
- You want to build an app using React.
- You want to give access to your application to signed-in users only.
Before you start, watch this video to see the user flow you're going to implement:
You can find the code of the sample application here.
Create React app
First, create a new React project:
npm create vite@latest -- --template react-ts
cd app-name
npm install
Create a new Ory project
- Create an Ory account at https://console.ory.sh
- Create a new project at https://console.ory.sh/projects/create
- Go to your project settings
- Note down your project credentials (ID, slug, endpoint)
Install Ory SDK and CLI
To interact with Ory's APIs, install the Ory SDK and CLI:
npm i --save @ory/client-fetch @ory/cli
Why do I need the Ory CLI
The Ory security model uses HTTP cookies to manage sessions, tokens, and cookies. Because of browser security measures like CORS, Ory APIs must be exposed on the same domain as your application.
In the case of this example the application runs on your local machine. The cookie domain is localhost
.
When developing locally, use either localhost
or 127.0.0.1
, but not both. Although technically these mean the same thing,
they're different cookie domains.
Using both interchangeably in your code causes problems, as Ory APIs will not receive the cookies when they are set on a different domain.
Ory CLI provides a convenient way to configure and manage projects. Additionally, the CLI provides Ory Tunnel - a reverse proxy that rewrites cookies to match the domain your application is currently on.
To make Ory APIs and your application available on the same domain, Ory Tunnel mirrors Ory endpoints and rewrites cookies to match the domain correct domain.
As a result, the domain of the cookies is set correctly to the domain you run the app on instead of
<your-project-slug>.projects.oryapis.com
.
By using the Tunnel, you can easily connect the application you're developing locally to Ory Network and consume the APIs without additional configuration or the self-hosting any Ory services.
To learn more about the Ory Tunnel, read the dedicated section of the Ory CLI documentation.
Run Ory APIs on localhost
In a new terminal window, start the Ory Tunnel:
npx @ory/cli tunnel --dev http://localhost:5173
This command mirrors Ory APIs on http://localhost:4000
. Use that URL as the baseUrl
for the @ory/client-fetch
SDK (see
App.tsx
code below). The --dev
flag disables security checks for easier integration and should not be used when running the
Tunnel in an insecure environment like public networks.
Authenticate user
To implement authentication, modify the existing App.tsx
component to include the following:
import { useState, useEffect } from "react"
import "./App.css"
import { FrontendApi, Configuration, Session } from "@ory/client-fetch"
// Props interface (matching Vue props)
interface AppProps {
msg?: string
}
const basePath = import.meta.env.VITE_ORY_URL || "http://localhost:4000"
console.log("basePath", basePath)
// Initialize Ory client
const ory = new FrontendApi(
new Configuration({
basePath,
credentials: "include",
}),
)
function App({ msg }: AppProps) {
// State variables
const [session, setSession] = useState<Session | null>(null)
const [logoutUrl, setLogoutUrl] = useState<string | null>(null)
const fetchSession = async () => {
try {
// Fetch the session directly from Ory
const data = await ory.toSession()
setSession(data)
// Create logout URL if session exists
const logoutData = await ory.createBrowserLogoutFlow()
setLogoutUrl(logoutData.logout_url)
} catch (error) {
console.error("Error fetching session:", error)
}
}
// Lifecycle hooks
useEffect(() => {
// Fetch the session and API response
fetchSession()
}, [])
return (
<div className="main">
<h1>{msg}</h1>
<div className={!session ? "" : "hidden"}>
<p>Click on "login" or "Sign Up" below to sign in.</p>
<li>
<a href={`${basePath}/ui/login`} data-testid="sign-in">
Login
</a>
</li>
<li>
<a href={`${basePath}/ui/registration`} data-testid="sign-up">
Sign Up
</a>
</li>
</div>
<div className={session ? "long" : "hidden"}>
<p>
Use the SDK's <code>toSession()</code> call to receive the session
information, for example the authentication methods used:
</p>
<pre>
<code data-testid="ory-response">
{session
? JSON.stringify(session.authentication_methods, null, 2)
: ""}
</code>
</pre>
</div>
<ul className={session ? "" : "hidden"}>
<li>
<a href={logoutUrl || "#"} data-testid="logout">
Logout
</a>
</li>
</ul>
<br />
<h3>Essential Links</h3>
<ul>
<a href="https://www.ory.sh">Ory Website</a>
<a href="https://github.com/ory">Ory GitHub</a>
<a href="https://www.ory.sh/docs">Documentation</a>
</ul>
</div>
)
}
export default App
The component checks whether a user is signed in and if so, shows a logout link and the user's session information. Otherwise, it shows a login and registration link.
Run your React app
Now that your app is ready, it's time to run it! Start the React development server:
npm run dev
Go to localhost:5173 to access your application.
Make authenticated calls to your API
To make authenticated requests to your API there are two main components:
- When making AJAX requests you must set
{"credentials": "include"}
in the options. For thefetch
method it looks like this:
import { useState, useEffect } from "react"
import "./App.css"
import { FrontendApi, Configuration, Session } from "@ory/client-fetch"
// Props interface
interface AppProps {
msg?: string
}
// State variables
const basePath = import.meta.env.VITE_ORY_URL || "http://localhost:4000"
const apiUrl = import.meta.env.VITE_API_URL || "http://localhost:8081"
const ory = new FrontendApi(
new Configuration({
basePath,
credentials: "include",
}),
)
function AppWithAPI({ msg }: AppProps) {
// Use proper Session type from the SDK
const [session, setSession] = useState<Session | null>(null)
const [logoutUrl, setLogoutUrl] = useState<string | null>(null)
const [apiResponse, setApiResponse] = useState<any | null>(null)
const fetchSession = async () => {
try {
const data = await ory.toSession()
setSession(data)
const logoutData = await ory.createBrowserLogoutFlow()
setLogoutUrl(logoutData.logout_url)
} catch (error) {
console.error("Error fetching session:", error)
}
}
const fetchApiHello = async () => {
try {
const res = await fetch(`${apiUrl}/api/hello`, {
// Do not forget to set this - it is required to send the session cookie!
credentials: "include",
})
if (res.ok) {
const data = await res.json()
setApiResponse(data)
}
} catch (error) {
console.error("Error fetching API response:", error)
}
}
// Lifecycle hooks
useEffect(() => {
fetchSession()
// Make an authenticated API call
fetchApiHello()
}, [])
return (
<div className="main">
<h1>{msg}</h1>
{!session ? (
<div>
<p>Click on "login" or "Sign Up" below to sign in.</p>
<li>
<a href={`${basePath}/ui/login`} data-testid="sign-in">
Login
</a>
</li>
<li>
<a
href={`${basePath}/ui/registration`}
data-testid="sign-up"
id="sign-up"
>
Sign Up
</a>
</li>
</div>
) : (
<>
<h3>
Calling <code>toSession()</code>
</h3>
<div className="long">
<p>
Use the SDK's <code>toSession()</code> call to receive the session
information, for example the authentication methods used:
</p>
<pre>
<code data-testid="ory-response">
{JSON.stringify(session.identity?.traits, null, 2)}
</code>
</pre>
</div>
{apiResponse && (
<>
<h3>API Response</h3>
<div className="long">
<p>
Make authenticated AJAX calls to your API using{" "}
<code>fetch()</code>:
</p>
<pre>
<code data-testid="api-response">
{JSON.stringify(apiResponse, null, 2)}
</code>
</pre>
</div>
</>
)}
{/* Add Common Actions section with logout link */}
<h3>Common Actions</h3>
<ul>
<li>
<a href={logoutUrl || "#"} data-testid="logout">
Logout
</a>
</li>
<li>
<a href={`${basePath}/ui/settings`} data-testid="settings">
Settings
</a>
</li>
</ul>
</>
)}
<h3>Essential Links</h3>
<ul>
<li>
<a href="https://www.ory.sh">Ory Website</a>
</li>
<li>
<a href="https://github.com/ory">Ory GitHub</a>
</li>
<li>
<a href="https://www.ory.sh/docs">Documentation</a>
</li>
</ul>
</div>
)
}
export default AppWithAPI
- Your API must have a CORS middleware with
credentials: true
andAccess-Control-Allow-Origin
of your frontend app (herehttp://localhost:5173
).
Let's put this into action. Create a simple HTTP API with express. Run:
mkdir api
cd api
npm init
npm i --save @ory/client-fetch express cors
Next, create a simple API in index.js
:
const express = require("express")
const cors = require("cors")
const { FrontendApi, Configuration } = require("@ory/client-fetch")
const app = express()
const ory = new FrontendApi(
new Configuration({
// Points to the local Ory API server (Ory TunneL).
basePath: process.env.ORY_URL || "http://localhost:4000",
baseOptions: { withCredentials: true },
}),
)
app.use(
cors({
origin: process.env.UI_URL || "http://localhost:8080",
credentials: true, // <- Required for CORS to accept cookies and tokens.
}),
)
app.use((req, res, next) => {
// A simple middleware to authenticate the request.
ory
.toSession({
// This is important - you need to forward the cookies (think of it as a token)
// to Ory:
cookie: req.headers.cookie,
})
.then(({ data }) => {
req.session = data
next()
})
.catch(() => {
res.status(401)
res.json({ error: "Unauthorized" })
})
})
app.get("/api/hello", (req, res) => {
res.json({
message: "Hello from our API!",
session_id: req.session.id,
identity_traits: req.session.identity.traits,
})
})
const port = process.env.PORT || 8081
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
Finally, start the server:
node index.js
See it in action
Access your react app at localhost:5173, sign in, and see if you can make authenticated requests to your API!
Go to production
To promote this app to production:
- Build the React app and run it somewhere (for example on Vercel or Netlify)
- Deploy the API (for example, on Heroku)
- Connect your project with a custom domain
These three components must be hosted on the same top-level domain as they were on your local machine:
Component | Production | Local |
---|---|---|
React App | www.example.org | localhost:5173 |
API | api.example.org | localhost:8081 |
Ory | ory.example.org | localhost:4000 |