Privasys
Privasys ID

Auth SDK

The browser SDK that adds Privasys ID sign-in and a sealed channel to an attested enclave app, in one call.

@privasys/auth is the browser SDK a web app drops in to sign people in with Privasys ID and, when the app runs in an attested enclave, to talk to it over a sealed channel. The sign-in surface and the session key live in an iframe on privasys.id; the page talks to that iframe over a small typed postMessage interface and never sees a key. There is no server-side dependency on the embedding domain.

For a step-by-step integration, read the tutorial.

What the SDK does

  • Signs the user in: wallet ceremony by QR code or push to a paired wallet, with passkey and federated sign-in as configurable alternatives.
  • Keeps the user signed in: tokens are held in the iframe on privasys.id, renewed silently, and shared across every site that loads the SDK (cross-site SSO).
  • Asks for attributes the app declares, with the user's consent in the wallet.
  • When the app runs in an enclave, establishes a sealed session: AES-256-GCM end to end from the iframe to the enclave, through a gateway that only ever carries ciphertext, with requests, server-sent streams and WebSockets all sealed.
  • Keeps that sealed session alive without the user: when the enclave forgets it (idle, restart) the SDK re-establishes it from a voucher the wallet issued at sign-in, and only involves the user again when the enclave's measurement changed.

Loading it

Either install the package or load the hosted client bundle. Both expose the same AuthFrame class.

<script src="https://privasys.id/auth/privasys-auth-client.iife.js"></script>
<script>
  const { AuthFrame } = window.Privasys;
</script>
import { AuthFrame } from '@privasys/auth';

The package is published to GitHub Packages. Apps that want one reviewed file in their image can vendor the hosted bundle instead: it holds no keys, and the part of the SDK that runs on privasys.id is served by Privasys and updated independently.

One frame, one call

Create one AuthFrame for the life of the page and call connect():

const frame = new AuthFrame({
  apiBase: 'https://api.privasys.org',
  authOrigin: 'https://privasys.id',
  rpId: 'privasys.id',
  clientId: 'my-app',                       // from your relying-party registration
  appName: 'My App',
  brokerUrl: 'wss://relay.privasys.org/relay',
  scope: ['openid', 'offline_access'],
  sessionRelay: { appHost: 'my-app.apps.privasys.org' },  // your enclave app
  container: document.getElementById('gate'),
  presentation: 'page',
  methods: ['wallet'],
  pitch: { title: 'Your data, sealed.', bullets: ['Nothing leaves the enclave in the clear.'] },
  app: { displayName: 'My App', logoUrl: 'https://my-app.example/logo.svg' }
});

const { accessToken, session } = await frame.connect();

connect() does whichever of these the situation needs, in order, and resolves once the app has what it asked for:

  1. Silent restore. An existing Privasys ID session (SSO) and, with sessionRelay, the sealed session re-established from the stored voucher. No UI.
  2. One-tap re-approval. The session exists but the enclave refused the voucher because its measurement changed (the app was updated, or the platform under it). The gate asks for one approval in the wallet and explains what changed.
  3. Sign-in. The full ceremony, rendered in container per presentation.

It rejects with a ConnectError whose code is cancelled, timeout or failed. To retry, call connect() again on the same frame.

OptionMeaning
sessionRelay.appHostThe enclave app's platform hostname. Always the platform name, never a custom domain the page may be served from.
scope, attributesWhat the sign-in asks for. Ask for the minimum: an attribute the user must first add to their wallet turns a returning visit into an onboarding.
presentationpage renders the whole gate in your container; inline a compact column; modal an overlay.
methodsRestrict to wallet, passkey or social. A sealed session needs the wallet, so enclave apps set ['wallet'].
pitch, appPlain strings and an https logo, rendered by the SDK in its own styling.

The sealed session

With sessionRelay set, connect() resolves with a SealedSession. The session the app gets carries the user's identity: the enclave asserts it to the app as the X-Privasys-Sub header on every request, so the app never handles a token on this path. The subject is your client's own for that person (see Subjects: one per application): stable for you, and meaningless to any other application.

const r = await session.request('GET', '/api/v1/me');      // { status, headers, body: Uint8Array, sealed }
const s = await session.stream('POST', '/v1/chat', body);  // ReadableStream of plaintext bytes (SSE etc.)
const ws = session.openWebSocket('/api/v1/events');        // sealed both ways; ready, send, onMessage, onClose

Every body leaves the browser as ciphertext in application/privasys-sealed+cbor. The HTTP status in request() is the app's own status, carried inside the sealed envelope. An unsealed 401 is different: it means the enclave no longer knows the session, and the SDK handles it itself (next section).

Liveness

A sealed session reports its state and survives its own recovery. Keep using the same object; never rebuild your transport on an error.

session.state.statusWhat it meansWhat the app does
okRequests flow.Nothing.
recoveringThe enclave forgot the session (idle timeout, restart) and the SDK is re-establishing it from the voucher. reason names a transient obstacle (unavailable, rate-limited).Show "reconnecting" if you like. A request made meanwhile may fail; the next one retries.
reapproval-requiredThe enclave refused the voucher. reason says why: workload-changed (the app was updated), enc-changed (the platform changed), voucher-expired, voucher-invalid, no-voucher.Call frame.connect() again. The gate runs the one-tap approval; the session object keeps working afterwards.
signed-outThe Privasys ID session ended.Call frame.connect() again; it signs the user in.
session.onState((s) => {
  if (s.status === 'reapproval-required' || s.status === 'signed-out') {
    void frame.connect({ container: gateElement }); // the gate you just mounted
  }
});

A sealed WebSocket refused because the enclave forgot the session is re-opened by the SDK on the recovered session, queued sends intact. Create one AuthFrame per page and per enclave host (sessionRelay.appHost is fixed at construction); the SDK warns when a second one for the same host is alive.

SSO across sites

Every site that loads the SDK shares the iframe origin privasys.id, so the session held there is visible to all of them: signing in once signs the user in everywhere, and frame.clearSession() signs them out everywhere. The shared state lives in the iframe's storage. The IdP takes no part in deciding who is signed in where; while a session is open it holds that session's renewal token, which it deletes at sign-out.

Because that session is shared, its renewal is coordinated across documents by the SDK. Create one AuthFrame per page; the SDK warns when a second is alive, because frames created and destroyed around each other are how a renewal is lost.

When the app declares attributes, the wallet shows a consent screen during sign-in and the user chooses what to share. Granted attributes flow into the JWT for that sign-in; refreshing the token does not re-include them. See the IdP page and the attribute marketplace for government-verified claims.

Trust scope of the iframe

The iframe lives on privasys.id. The page cannot read its storage, its WebCrypto keys or its session table; it talks to the iframe only over postMessage. The sealed session's key and the SDK's ephemeral private key never cross that boundary: the page submits plaintext and receives plaintext, and sealing happens inside. The trust argument, including why the gateway can terminate TLS without seeing anything, is on Technology > Sealed Session.

Source

@privasys/auth is part of github.com/Privasys/identity-platform, in sdk/, AGPL-3.0. The hosted bundles are served from https://privasys.id/auth/.

Edit on GitHub