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:
passkey-seed: the passkey is the identity. Its PRF output is the seed, so there is nothing stored to steal and nothing to unseal.passkey: Touch ID, Face ID or Windows Hello returns a secret that unwraps a sealed phrase or three-box secret.device: a non-extractable key in IndexedDB unwraps it, with no prompt. Script cannot read the key out the way it readslocalStorage, 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().