Skip to content

Omxus identity in your own design

Works on the provider's own origin only, today

omxus-credential.mjs is not served at auth.omxus.com/sdk/ (404, checked 30 Oct 2026) and is not on npm. The provider's /auth/challenge and /auth/verify send no CORS headers, so a page on another origin cannot finish a sign-in against auth.omxus.com. Everything below works on a page served by the provider itself, such as your own provider. On any other site, sign people in with OIDC or Signed in everywhere.

This page is for the Omxus identity: a key derived from a passkey, a phrase or three inputs, held on the device. For ordinary accounts (email or mobile, username, password or passkey) on your own page, see Your own sign-in page.

Sometimes a redirect is wrong. You want the sign-in inside your page, in your layout, with your type and your colours, and you still do not want to write any cryptography.

That is what omxus-credential.mjs is for. It has no DOM, no CSS and no framework. You render everything; it holds the rules.

The rule the design has to respect

Nothing secret is typed twice on the same device.

A page that asks for twelve secret words at every visit trains people to type their words into whatever page asks, and that habit makes a recovery phrase easier to steal than a password. It also loses to the Google button on the first screen, before anyone compares security.

So the SDK seals the secret on the device the first time it is typed, and after that a sign-in is one press.

The whole integration

import 'https://auth.omxus.com/engine/engine.bundle.mjs';   // engine first
import { createCredentialStore } from '/sdk/omxus-credential.mjs';

const cred = createCredentialStore({ issuer: location.origin });

// One button, nothing typed:
const session = await cred.signIn();

// Or, from your own fields:
// await cred.signInWithIdentity({ name, dob, words });   // the three boxes
// await cred.signInWithPhrase(phrase);                   // a recovery phrase

Each call signs in and seals in one go. Every call above was run against the provider's router in Node with a stand-in authenticator (30 Oct 2026); the browser prompts themselves are untested here.

What your screen needs

One button. cred.signIn() uses this device's passkey, makes one if there is none, and falls back on its own when the passkey cannot carry a secret. Most sites need nothing else.

Returning device, sealed secret. cred.isEnrolled() is true. Show one button that calls cred.unlockAndSignIn(). Do not show the fields; there is nothing for them to fill in.

New device, or recovery. Your own three fields, or a phrase. To make a new phrase: await cred.createPhrase(), and show it once. For a typed phrase, await cred.validatePhrase(phrase) returns { valid, error }; show error as it is. It names a word that is not on the list ("kettel" is not a recovery-phrase word.) and catches most swapped real words through the checksum (That phrase has a typo in it — the checksum does not match.). A wrong phrase that slips past derives a different, empty account, which looks exactly like a stolen one.

An error line. Every call throws a message written to be shown to a person, for example Too easy to guess. About 14 more characters would do it — or mix in something other than just letters and digits.

How it remembers, chosen for you

tier() says which one this device got:

  1. passkey-seed: the passkey is the identity. Its PRF output is the seed, so there is nothing stored to steal and nothing to unseal.
  2. passkey: Touch ID, Face ID or Windows Hello returns a secret that unwraps a sealed phrase or three-box secret.
  3. device: a non-extractable key in IndexedDB unwraps it, with no prompt. Script cannot read the key out the way it reads localStorage, so a copy of the browser profile is not a copy of the secret.

You do not choose. The SDK takes the best the device has and falls back on its own, including when the biometric prompt is never answered: navigator.credentials.create() can return a promise that never settles, so every prompt is bounded by passkeyTimeoutMs (default 60 s).

A passkey-seed identity lives only in its passkey. Offer cred.addRecovery({ name, dob, words }) straight after the first sign-in, so losing every device is not losing the account. It changes the identity to the one the three boxes make; see SDK reference.

What crosses the wire

One signature. The secret stays in the page. The signed bytes name the provider's origin, this site's address, the provider's nonce, when it was issued and how long it lasts. The provider checks the origin against its own (never the request's word for it) and spends the nonce once, so a signature made for one site is worth nothing at another, and worth nothing twice.

A worked example

The provider's own sign-in screen is one: frontend/src/App.tsx in the auth repo. Its passkey buttons call signInWithPasskey() and createPasskeyAndSignIn(), the three boxes call signInWithIdentity() or addRecovery(), and a sealed phrase goes through signInWithPhrase().