- The Client Session Token never reaches your page. The SDK runs in a hidden
iframe on the Portal origin, and the token stays there.
portal.apiKeystaysundefined. portal.authis the host-facing API. Every method proxies into the iframe, because/api/v3/auth/*is not reachable cross-origin from your page.restoreSession()returns identity, not a credential — anendUserIdand nothing more.
Portal constructor after signing in: the same
instance you configured is the one that becomes authenticated.
Client Auth requires a version of
@portal-hq/web whose Portal accepts a
clientAuth option. If portal.auth is undefined on your installed version,
upgrade to the latest release.Prerequisites
- Authentication enabled for your environment, with at least one sign-in method and your Auth Environment ID to hand (see Enable authentication)
- The sign-in methods you want configured — Email magic links, Google OAuth, or Apple OAuth
- A redirect URL on your environment’s allow list, pointing at a callback route in your app (see Redirect URLs)
- A working Portal integration (see Getting Started)
Choose an authentication mode
The Web SDK accepts four credential modes. They are mutually exclusive.
The first three assume you already have a client and are authenticating it; see
Web authentication methods. Use
clientAuth
when you want Portal to identify the user and create the client as part of the
sign-in.
Configure Portal for Client Auth
PassclientAuth instead of a credential. The iframe boots unauthenticated and
waits for a sign-in.
isAccountAbstracted only takes effect on a user’s first sign-in. A returning
user keeps the client they already have.Handle the unauthenticated first state
Read this before writing any sign-in code. APortal configured for Client Auth posts authenticationRequired during
configuration and stops there — it does not become ready until a session is
adopted. onAuthenticationRequired is replayed to late subscribers, so you
cannot miss it by subscribing a moment too late.
Three signals are available, and each answers a different question:
onReady answers a question about the SDK, not about your user. Do not treat it
as proof that someone is signed in — see the warning below.
What a session begins is the result of handleRedirect() or
restoreSession(). Those return values are the authoritative signal that
someone is now signed in, so keep the answer in one place your app owns:
Until a session is adopted the iframe never reaches
ready, so a loading state
that waits only for onReady never resolves. Gate wallet and API calls on
onReady; gate your sign-in UI on onAuthenticationRequired.Check which methods are enabled
autoCreateWallet is a signal for your app — nothing in the SDK or the API acts
on it. Check for an existing wallet before creating one; a returning end user
keeps the wallet they already have. See Create a wallet.
A two-factor requirement is not visible here. It only appears when a
sign-in is completed, so your callback route must always be ready for it.
Start a sign-in
Google or Apple
getAuthorizeUrl() returns a provider URL for your app to navigate to. The
sign-in completes later, on your callback route.
Offer these controls only while nobody is signed in — see
Handle the unauthenticated first state.
Email magic link
sendMagicLink() resolves once Portal has handed the email off for delivery. It
tells you nothing about the eventual sign-in, which completes when the user
opens the link in a browser.
Every call sends a real email, and sends are rate limited to 10 per address per
minute. Never retry automatically; make a resend an explicit user action.
Build the callback route
Add a route at the exact URL on your allow list. It hands its own URL tohandleRedirect(), which owns extracting the grant and exchanging it.
Because the page loads fresh, wait for the SDK before calling it — and subscribe
to both onReady and onAuthenticationRequired, since either can fire
first depending on whether a session was already stored.
handleRedirect() resolves null when the URL does not carry a Client Auth
grant, so it is safe to reach from a shared route.
Handle two-factor authentication
If your environment requires a second factor, a completed sign-in resolves withstatus: 'totpRequired' instead of a session.
totpLink is an otpauth:// URI on a user’s first sign-in — render it as a QR
code for their authenticator app — and null once they are enrolled.
Rendering the QR code is your app’s job; Portal does not ship a QR component.
Any library works —
qrcode.react is used above — and challenge.totpLink
should be passed verbatim.Restore the session
restoreSession() asks the iframe whether it still holds a stored session for
this auth environment. The SDK never calls it for you.
Its result is authoritative in the same way the redirect result is, so hand it
to the same adopt path:
Sign out
onAuthenticationRequired handler fires again and clears the signed-in state
you set with adopt.
There is no server-side revoke endpoint, so this is a local sign-out; the token
stays valid until the backend expires it.
clearSession() deliberately does not
fire onSessionInvalidated — you already know about a sign-out you asked for.
See Authentication and API Keys
for session lifetimes.Signing out does not delete the wallet’s signing shares. The session lifecycle
and the wallet lifecycle are separate: when the user signs in again, they
continue with the same wallet, provided that wallet is otherwise still available
to them.
Handle session invalidation
When the backend rejects the session with a401, it ended somewhere your app
cannot see and the user has to sign in again. The subscription belongs in
SignedInUserProvider from
Handle the unauthenticated first state,
which is where the example above puts it.
As with a sign-out, an invalidated session does not delete the wallet’s signing
shares — only the credential is gone. Wallet state queries cannot be answered
without a session, so treat them as unknown until the user signs in again rather
than as evidence the wallet is missing.
Handle errors
Credential failures are normalized toPortalCredentialError. Branch on its
reason.
Error from handleRedirect() rather than a dedicated type, so catch
and report it rather than branching on it. Popup helpers throw
PortalAuthPopupError, which you will not encounter using the redirect flow
above.
The Web and React Native Client Auth APIs are not the same shape — different
entry point, different sign-out, and a different
restoreSession() return type.
Do not port code between them; use each platform’s guide.