Skip to main content
With Client Auth, Portal identifies your end user and creates their client for you. Your app asks Portal to start a sign-in, receives the completed sign-in as a deep link, and gets back a PortalSession that it passes straight to Portal. No login system of your own, and no server-side call to mint a Client Session Token. Your app owns three things the SDK deliberately does not: when a sign-in starts, how the authorize URL is opened, and how the redirect gets back to you. PortalAuth never opens a browser and never registers a deep-link handler.
Client Auth requires a version of @portal-hq/core that exports PortalAuth. If PortalAuth is not exported by your installed version, upgrade to the latest release.

Prerequisites

Installation

There is nothing new to install. PortalAuth ships in @portal-hq/core, and the session is stored with react-native-keychain, which @portal-hq/keychain already requires.

Register your redirect

The OS only delivers a redirect to your app if you register its scheme natively. For a redirect URL of myapp://auth/callback, register the scheme and make sure a returning redirect reaches the running app.
Register the scheme in Info.plist:
Registering the scheme is not enough on its own. A cold start arrives through launchOptions, which Linking.getInitialURL() reads, but the usual Client Auth path — the browser returning to an app that is still running in the background — needs your AppDelegate to forward the URL:
Without this, Linking.addEventListener('url', …) never fires for a warm or backgrounded app and the sign-in appears to hang.The notification is posted directly because React-RCTLinking ships only its .m/.mm sources, so RCTLinkingManager.h is not a public header and the class is not visible from Swift. In an Objective-C AppDelegate you can call [RCTLinkingManager application:app openURL:url options:options] instead — it posts the same notification.
The redirect URL must be byte-identical in three places: your environment’s allow list in the dashboard, the redirectUrl you pass to PortalAuth, and the native registration above. A mismatch between the first two fails the sign-in with a 401; a mismatch between the last two means the redirect silently never reaches your app. See Redirect URLs.

Create a PortalAuth

Create one long-lived PortalAuth and reuse it. It holds no in-flight state, so concurrent sign-ins are safe and a sign-in survives your app being backgrounded.
isAccountAbstracted is fixed when you construct PortalAuth, and it only takes effect on a user’s first sign-in. A returning user keeps the client they already have.

Check which methods are enabled

getMethods() reports what your environment allows, so you can render only the buttons that will work.
autoCreateWallet is a signal for your app — nothing in the SDK or the API acts on it. See Create or reuse the wallet.
A two-factor requirement is not visible here. It only appears when a sign-in is completed, so your redirect handler must always be ready for it.

Start a sign-in

Google or Apple

loginWithGoogle() and loginWithApple() return an authorize URL. They do not open a browser and do not wait for the sign-in to finish — your app opens the URL, and the result arrives later as a deep link.
Fetch a fresh authorize URL for every attempt. A single backend response shares one single-use state across both providers, so caching a URL — or starting a Google sign-in and then an Apple one from the same response — invalidates the other.
Linking.openURL hands off to the system browser, which is what the Portal example app does and what Google’s OAuth policy requires. You can instead use a native auth-session API such as ASWebAuthenticationSession or Android Custom Tabs, but be careful: on Android the Custom Tab redirect also fires your intent filter, so the session’s own result and your deep-link listener will both deliver the same redirect. Feed it to handleRedirect() from one path only — see Complete the sign-in.
sendMagicLink() resolves once Portal has handed the email off for delivery. It tells you nothing about the eventual sign-in, which arrives as a deep link when the user opens the link.
Lowercase and trim the address before you call sendMagicLink(). The API rejects a non-lowercase address with a 400, and the SDK passes what you give it through unchanged — so a plain text input breaks for any user who capitalizes.
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.

Complete the sign-in

handleRedirect() is the single completion path for every method. Give it the incoming URL and it either resolves a result, resolves null because the URL was not a Client Auth redirect, or throws. Register the listener before you open any browser, and read getInitialURL() once per process in case the redirect is what launched your app. Keep that latch at module scope rather than in component state: getInitialURL() keeps returning the launch URL for the lifetime of the process, so a per-component guard lets a remount replay a grant that has already been spent.
Route every redirect through exactly one handleRedirect() call. A grant is spent the moment the backend sees it, so a second call on the same URL throws GrantRejected — which looks like a real authentication failure to your user. Operating systems do re-deliver deep links, so this matters in practice.
handleRedirect() resolves null for any URL that is not this instance’s redirect, which makes it safe to call from a shared deep-link handler alongside your app’s other routes.

Handle two-factor authentication

If your environment requires a second factor, a completed sign-in resolves with status: 'totpRequired' instead of a session. Nothing is persisted yet. 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. On success, verifyTotp() resolves the same shape the non-TOTP path produces, so result.session is the PortalSession you carry on with — hand it to the same place you would hand a session from handleRedirect():
Wired into the hook from the previous section, both paths converge on the one createPortal from Initialize Portal from the session:
Rendering the QR code is your app’s job; Portal does not ship a QR component. Any React Native QR library works — pass challenge.totpLink to it verbatim.
A rejected code, an expired userJwt, and one that has already been used all surface the same way, so the SDK cannot tell you which happened. Prompt for the next code from the authenticator app; if that keeps failing, start a new sign-in. There is no way to refresh a userJwt.
For what the second factor is and how to reset an enrollment, see Two-factor authentication.

Initialize Portal from the session

Pass the session as credentials. Everything else about Portal is unchanged — including backup, which is still required.
credentials and apiKey are mutually exclusive — passing both throws.
portal.apiKey is an empty string on a session-backed instance and is deprecated. Do not read it or forward it anywhere, such as into a webview: it carries no credential.

Create or reuse the wallet

A returning end user keeps the client and wallet they already have, so check for addresses before creating anything. Create a wallet only when the environment asks you to, via autoCreateWallet.
See Create a wallet for the full wallet lifecycle.

Restore the session on launch

restoreSession() rebuilds the session from secure storage so a returning user does not sign in again.
A restored session is a credential worth trying, not proof that it is still valid. Validity is only discovered on the first authenticated call. Note also that corrupt storage makes restoreSession() reject rather than resolve nullnull means “not signed in”, a rejection means “storage is broken”.

End the session

There are three ways a session ends, and they are not interchangeable.
clearPersistedSession() is not a sign-out. It stops a future restoreSession() from returning the session, but a Portal already holding the credential keeps signing transactions. To sign a user out, call portal.clearSession().
There is no server-side revoke endpoint, so both operations are local sign-outs. The token itself stays valid until the backend expires it. See Authentication and API Keys for session lifetimes.
Neither operation deletes 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 credential with a 401, the session ended somewhere your app cannot see and the user has to sign in again.
The returned function unsubscribes. The callback fires at most once for an instance, and it is not fired by portal.clearSession() — you already know about a sign-out you asked for. Subscribe from something that lives as long as the Portal it belongs to. The callback is not replayed, so if the only subscriber is inside a screen that has been unmounted by the time the session ends, the event is missed and your app carries on looking signed in.
credentials is set once at construction, so a Portal whose session ended stays spent: every later call fails with SessionInvalidated. After the next sign-in, construct a new Portal and subscribe again rather than reusing the old instance.
As with a sign-out, an invalidated session does not delete the wallet’s signing shares — only the credential is gone. Wallet state cannot be read without a session, so treat it as unknown until the user signs in again rather than as evidence the wallet is missing.

Handle errors

Client Auth surfaces two error types. Branch on their reason. PortalAuthError — a sign-in that could not be completed: PortalCredentialError — a credential that could not be turned into a token:
Use PortalAuthError.is(error) and PortalCredentialError.is(error), never instanceof. Both types are identified by a cross-copy brand, so instanceof returns false when a dependency tree resolves two copies of @portal-hq/utils — silently downgrading a real SessionInvalidated to an unhandled error.

Next Steps

Now that your user is signed in, create a wallet and send tokens. For the dashboard and provider configuration behind this flow, see Authentication.