@userspace-auth/react
v1.12.9
Published
React SDK for Userspace Auth, session state, tokens, and auth guards
Downloads
1,029
Maintainers
Readme
Userspace React SDK (@userspace-auth/react)
This SDK gives session state, session tokens and auth guards to a React application that uses Userspace.
The sign-in occurs on the hosted pages of your Userspace instance. This package tells your application who is signed in and gives you a session token for calls to your backend.
Install
npm i @userspace-auth/reactReact 18 or 19 is necessary, and the package declares them as peer dependencies. The
only runtime dependency is @userspace-auth/userspace-js. The package imports it lazily
for the x-* elements and the settings popup. Thus a page that only uses session tokens
never loads it.
The package is ESM only. It works in Vite, Next.js, Remix, Parcel, esbuild, webpack 5
and each other tool that resolves import. A CommonJS-only setup cannot require() it.
An example is an older Jest config without ESM support.
Quick start
Wrap your application one time:
import { UserspaceProvider } from '@userspace-auth/react'
export function App() {
return (
<UserspaceProvider baseUrl="https://acme.userspace.tech">
<Dashboard />
</UserspaceProvider>
)
}Then read the session in any component below the provider:
import { useSession, SignedIn, SignedOut } from '@userspace-auth/react'
function Dashboard() {
const { session, isLoading } = useSession()
if (isLoading) return <p>Loading…</p>
return (
<>
<SignedIn>
<p>Signed in as {session?.identity?.email}</p>
</SignedIn>
<SignedOut>
<a href="https://acme.userspace.tech/sign-in">Sign in</a>
</SignedOut>
</>
)
}Calling your backend
Your own backend needs no token at all
By default, <UserspaceProvider> keeps the session token of the signed-in user in a
__Host-session cookie on your host. The provider refreshes the cookie before the
session token expires. Your backend reads the session token from the cookie. Thus a page
load, a form post or a plain fetch to your origin is authenticated:
// No Authorization header: the browser sends the cookie on its own.
const res = await fetch('/api/me')The cookie transport is off by default in your backend. Set it to on. For Go:
u, err := userspace.New(userspace.Options{CookieTransport: true})The cookie transport has two effects. First, your backend refuses a request that changes
state (POST, PUT, PATCH, DELETE) when the request sends the cookie from a
different origin. This prevents an action by a different site in the name of your user.
Second, your backend gets a third answer together with signed in and signed out.
The third answer: "unknown". The session token in the cookie has a life of minutes.
A browser that was idle can send a request with no session token while the user is
signed in. This occurs because the SDK did not refresh the session token yet. Your
backend reports this state as unknown, not as signed out.
This is important for pages that you render on the server. For React, these are the App
Router and getServerSideProps. After a pause longer than the life of the session
token, the first request renders unknown. The page becomes correct a moment later,
when this SDK refreshes the session token on the client.
Thus render unknown with your application shell, not with your sign-in page. If you
render the sign-in page, a signed-in user sees it for a moment. A server-side refresh
that fully removes this flicker is not available yet.
unknown never gives permission for an action. For each authorization decision, use
unknown the same as signed out. The backend SDK does this.
Running on bearer headers alone
Set cookie={false}. Then the provider writes no cookie, runs no refresh timer, and
gets a session token when a call asks for one. Use this mode if you only call an API on
a different origin, or if you do not want a cookie on your host.
The SDK also uses this mode automatically in one condition. __Host- cookies must have
Secure. If the browser does not store such a cookie on your page, the SDK continues as
with cookie={false}. The SDK writes one message about this to the console. This is
important for development on plain http://localhost, where browsers do not agree if a
page can set a Secure cookie.
Sending the token yourself
getToken() returns a session token that your backend can verify locally. Use it for an
API on a different origin, where the browser sends no cookie of yours. Send the session
token as a bearer token:
function Profile() {
const { getToken } = useSession()
const load = async () => {
const token = await getToken()
if (!token) return // signed out
const res = await fetch('https://api.example.com/me', {
headers: { Authorization: `Bearer ${token}` },
})
return res.json()
}
// …
}From your api layer, outside React
Your data layer can be a plain module of functions that use one shared fetcher. Then
import getToken directly, and do not pass a hook through the data layer. It is the
same function that useSession() returns, and thus the two cannot become different:
import { getToken } from '@userspace-auth/react'
async function authorized(path: string, init: RequestInit = {}) {
const token = await getToken()
const headers = new Headers(init.headers)
if (token) headers.set('Authorization', `Bearer ${token}`)
return fetch(`https://api.example.com${path}`, { ...init, headers })
}
export const fetchUsers = () => authorized('/users').then((r) => r.json())If you call getToken before <UserspaceProvider> renders, it waits until the provider
configures the SDK. It does not send a request to a base URL that is not fully
configured. The provider stays the only location of the configuration. If the provider
never mounts, the call writes a warning after a few seconds and does not wait with no
sign.
On the backend, verify the session token with the Userspace SDK for your stack. For Go,
that SDK is github.com/userspace-auth/sdk-go. Any JOSE library also works. The session
token is a standard RS256 JWT, and your instance publishes the keys that sign it at
https://<your-instance>/.well-known/jwks.json.
Send this session token only to your backend. A session token gives access to no API of the platform. The Frontend API, which is the auth server of your instance, accepts only its own token with a separate signature. But each backend with the same base URL accepts the session token until it expires.
API
<UserspaceProvider>
| Prop | Type | Meaning |
| --------------- | ----------- | ------- |
| baseUrl | string | The base URL of your instance, for example https://acme.userspace.tech. |
| webcomponents | boolean | Registers the x-* custom elements (<x-signed-in>, <x-trigger>, ...). Then you can use them in JSX directly. Off by default. |
| preload | boolean | Loads the bundle of the settings popup when the browser is idle. Then the first openSettings() opens without a download. Off by default. |
| autoLogin | boolean | Redirects signed-out visitors to the hosted sign-in page, with the current URL as redirect_uri. Off by default. |
| cookie | boolean | Keeps the session token in a __Host-session cookie on your host, and refreshes the cookie before the session token expires. Thus your backend can read it. On by default. With false, the SDK uses only bearer headers, writes no cookie and runs no timer. |
| children | ReactNode | Your application. |
The provider must wrap each component that uses this package. Outside the provider, hooks throw a clear error and do not return an empty session.
webcomponents loads the element bundle lazily. When the flag is off, the browser
downloads none of the bundle. The tags have TSX types when the flag is on and when it is
off.
autoLogin reads the session one time on mount, and it shares the request that
useSession() makes. It redirects only when the result is signed out. A transport error
is an unknown state, not a signed-out state, and never causes a redirect. Use
autoLogin for applications with no public pages. For applications with a signed-out
state, render <SignedOut> and do not use autoLogin.
useSession()
const { session, getToken, isLoading, error } = useSession()| Field | Type | Meaning |
| ----------- | ------------------------------- | ------- |
| session | SessionData \| null | The session, or null while the session loads or when the user is signed out. |
| getToken | () => Promise<string \| null> | Returns a session token for your backend, or null if the user is signed out. Rejects on transport errors. |
| isLoading | boolean | True until the first load is complete. |
| error | unknown | A real failure. The signed-out state is not an error. It is session: null with error: null. |
session.identity.id is the stable user id. Use it as the key of your data. Never use
session.session.id as the key, because it changes at each sign-in.
getToken()
import { getToken } from '@userspace-auth/react'The type is () => Promise<string | null>. It is the same function that useSession()
returns. You can import it without a hook, and thus a fetcher at module scope can use
it. It resolves to null when the user is signed out, and it rejects on transport
errors. If you call it before <UserspaceProvider> configures the SDK, it waits for the
provider. It works only on the client, and Next.js gives the cause.
openSettings()
import { openSettings } from '@userspace-auth/react'
;<MenuItem onClick={() => openSettings()}>Account settings</MenuItem>openSettings() opens the Userspace settings popup from your components: a profile menu
item, a toolbar button, or any element that the user can click. All callers share one
popup instance. It works with the webcomponents flag and without it, because the popup
code loads lazily on the first call.
Give preload to the provider to move that download (and the config load of the SDK) to
idle time. Then the first click opens the popup immediately. The popup opens immediately
and loads its content in position. Without an active session, the popup closes again and
writes a console warning. Thus keep the control in <SignedIn>.
signIn()
import { signIn } from '@userspace-auth/react'
;<Button onClick={() => signIn()}>Sign in</Button>signIn() goes to the hosted sign-in page. It sends the current URL as redirect_uri,
and thus the user returns to the same page after the sign-in. With
signIn({ redirectUri }), the user goes to a different address. Use this for a page
that sends the visitor to a different location. redirectUri must be an absolute URL,
and its origin must be on the allowlist of the instance. The server validates the
target, and a target that the server does not trust becomes the app URL.
signIn() is the function equivalent of the autoLogin flag. It does the same
navigation, but on a click and not on each signed-out visit. If you call it before
<UserspaceProvider> configures the SDK, it waits for the provider, the same as
getToken().
signOut()
import { signOut } from '@userspace-auth/react'
;<MenuItem onClick={() => signOut()}>Sign out</MenuItem>signOut() signs the user out and goes to the hosted sign-in page. With
signOut({ redirectUri }), it goes to an absolute URL whose origin is on the allowlist
of the instance. The server validates the target, and a target that the server does not
trust becomes the sign-in page.
A sign-out stops each session in this browser, not only the current session. The server
stops the full cookie-scoped session, and thus other tabs also sign out. If you call
signOut() before <UserspaceProvider> configures the SDK, it waits for the provider,
the same as getToken().
<SignedIn> / <SignedOut>
Each component renders its children only in its own state. The two components render
nothing while the session loads. Thus no component shows for a moment before the session
is known. A session with an error is signed out for these components. SignedIn never
renders on an unknown state.
Types
The package exports SessionData, SessionInfo, UserspaceUser, EmailAddress,
UseSessionResult, UserspaceProviderProps, GuardProps, and UserspaceTagName. The
package includes its own declarations. Thus strict: true works with no @types/*
package and no any.
Caching behaviour
The SDK fetches the session one time for each page load and keeps it. Each component
that calls useSession() shares that one request. This is also true when many
components mount at the same time on a cold cache.
These functions are intentionally not included: revalidation on window focus or reconnect, polling, and manual mutation. If the SDK must read the session again after an action that changes it, reload the page or move that state into your own store. This contract is intentionally smaller than the contract of a general data-fetching library. As a result, the package has no data-fetching dependency. Thus the package cannot conflict with the data layer that you use.
The session token is a different subject, and the SDK does refresh it automatically. When the cookie transport is on, the SDK refreshes the session token on a timer before it expires. The timer stops while the tab is hidden, and the SDK refreshes the session token when the tab shows again. When the cookie transport is off, the SDK gets a new session token each time a call needs one. These refreshes do not change the session that this section describes.
Next.js
The bundle has a 'use client' directive. Thus you can import it from an App Router
server component, and you do not have to add the directive. Components that use
useSession are client components, the same as all components that use hooks.
The directive does not change server rendering. Next.js prerenders and hydrates client components as usual, and this package renders a defined loading state on the server.
The directive does prevent a call to getToken from a server component, a server action
or a route handler. This is intentional, not an error. The token cache and the
configured base URL are module singletons. Thus in a long-lived server process, the
session token of one request stays in the same cache that the next request reads.
To serve session tokens on the server, each call must get the identity of its request. That is a different contract, not a second build of this contract. Use this package from the client, and let your server read the session that your backend trusts.
License
MIT. Refer to LICENSE.
