Lynx/Modules/Web Auth/Usage
@sigx/lynx-webauth · Stable

Using Web Auth#

Open a system web-auth sheet for an OAuth / OpenID-Connect flow and read the callback URL inline — no global deep-link listener, no polling.

Basic usage#

openAuthSession(authorizeUrl, callbackScheme, options?) presents the OS web-auth UI over your app, waits for the provider to redirect to callbackScheme://…, then dismisses itself and resolves with the callback URL. The callbackScheme is the bare scheme you registered in signalx.config.ts (see Installation).

The call always resolves to a three-way result, so the common "user backed out" case needs no try/catch:

TSX
import { openAuthSession } from '@sigx/lynx-webauth';

const result = await openAuthSession(
  // Query params must be percent-encoded — redirect_uri=myapp%3A%2F%2Fcb here.
  // The PKCE recipe below builds the URL safely with URLSearchParams.
  'https://provider.com/authorize?response_type=code&client_id=…&redirect_uri=myapp%3A%2F%2Fcb',
  'myapp',
);

if (result.url) {
  // Provider redirected back — parse the callback URL.
  const code = new URL(result.url).searchParams.get('code');
  // exchange `code` for tokens via your backend
} else if (result.canceled) {
  // user dismissed the sheet
} else {
  console.error(result.error); // the session could not start or failed
}

The result is discriminated by which field is present — { url }, { canceled: true }, or { error } — so a truthy result.url is the signal that the provider redirected back.

PKCE authorization-code recipe#

For the standard PKCE authorization-code flow, the opt-in @sigx/lynx-webauth/oauth subpath covers the frozen parts of the spec — PKCE generation (RFC 7636, S256), a random state, and callback parsing. Token exchange is provider-specific and stays in your app. The helpers are pure JS and tree-shake away when unused.

TSX
import { openAuthSession } from '@sigx/lynx-webauth';
import { generatePKCE, generateState, parseCallback } from '@sigx/lynx-webauth/oauth';

async function signIn() {
  const { verifier, challenge, method } = await generatePKCE(); // method: 'S256'
  const state = generateState();

  // Build the query with URLSearchParams so every value — including the
  // redirect_uri scheme — is correctly percent-encoded.
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: '…',
    redirect_uri: 'myapp://cb',
    scope: 'openid profile',
    state,
    code_challenge: challenge,
    code_challenge_method: method,
  });
  const authorizeUrl = `https://provider.com/authorize?${params}`;

  const result = await openAuthSession(authorizeUrl, 'myapp');
  if (!result.url) return; // canceled or error — nothing to exchange

  const { code, state: returned, error } = parseCallback(result.url);
  if (error) throw new Error(error);
  if (returned !== state) throw new Error('state mismatch'); // CSRF / mix-up guard
  if (!code) throw new Error('no authorization code in callback');

  // POST { code, code_verifier: verifier } to YOUR token endpoint, then store
  // the tokens — e.g. with @sigx/lynx-secure-storage.
  return { code, verifier };
}

parseCallback merges params from both the ?query and the #fragment, so implicit / token-in-fragment responses are covered too. Keep the verifier until token exchange and send it as code_verifier; never put it in the authorize URL.

Customizing the sheet#

options tailors the per-platform UI; each flag is ignored on the platform it doesn't apply to:

TSX
await openAuthSession(authorizeUrl, 'myapp', {
  ephemeral: true,                               // iOS: fresh login, no SSO cookies
  toolbarColor: '#0088ff',                       // Android: Custom Tabs toolbar color
  preferredBrowserPackage: 'com.android.chrome', // Android: pin a browser
});
  • ephemeral (iOS) — don't share or persist the system browser's cookies, so the user always sees a fresh login. Use it for "add another account" flows.
  • toolbarColor / preferredBrowserPackage (Android) — theme the Custom Tabs toolbar, or pin a specific Custom Tabs provider instead of the user's default browser.

Cancelling an in-flight session#

Pass an AbortSignal to tear down a session you no longer need (e.g. the user navigated away). Aborting resolves the promise with { canceled: true }:

TSX
const controller = new AbortController();
const pending = openAuthSession(authorizeUrl, 'myapp', { signal: controller.signal });

// later…
controller.abort(); // resolves `pending` as { canceled: true }

If the signal is already aborted, the call resolves immediately without opening a browser.

Android limitation. Chrome Custom Tabs can't be force-dismissed programmatically, so on Android abort() settles the promise as { canceled: true } right away while the system tab stays up — it returns to the foreground on its own when the user dismisses it. On iOS, abort dismisses the ASWebAuthenticationSession sheet directly.

Guarding for availability#

isWebAuthAvailable() is a synchronous check of whether the native module is wired into the current build — use it to fail gracefully where the module was not linked:

TSX
import { isWebAuthAvailable } from '@sigx/lynx-webauth';

if (isWebAuthAvailable()) {
  const result = await openAuthSession(authorizeUrl, 'myapp');
  // …
}

See also#

  • API reference — every export, option, and result shape with its full signature.
  • Linking — the URL-scheme pipeline the callback rides in on.
  • Secure Storage — encrypted at-rest storage for the tokens you exchange the code for.