Skip to content

The drop-in button

For a site that does not speak OIDC, or one that wants sign-in to happen where the person already is rather than on a login page.

Not ready for sign-in (checked 30 Oct 2026)

Loaded on a page today, the button shows "omxus is unavailable": it imports https://auth.omxus.com/sdk/omxus-credential.mjs, which answers 404. And even once that file is served, what the button posts to your server carries no proof your server can check (see What your endpoint receives). Until both are fixed, sign people in with OIDC or Signed in everywhere.

One tag

<script src="https://auth.omxus.com/button.js"
        data-omxus
        data-verify="/api/auth/omxus"
        data-label="Login"
        data-reload></script>
attribute meaning
data-verify your endpoint, below. Default /api/auth/omxus
data-label the word on the button. Default Login (Add to Omxus for the wallet look)
data-variant login (round button, default) or wallet (pill with the Omxus mark)
data-home where the small "omxus" link under the button opens, in its own window. Default https://auth.omxus.com
data-reload reload on success; leave it off and listen instead
data-omxus-mount put on any element to render the button there; otherwise it renders where the tag sits

Style it with variables, or leave it:

:root{
  --omxus-size:132px;   /* round button only */
  --omxus-face:#0d8b88;
  --omxus-ring:#d4af37;
  --omxus-word:#fff;
}

Listen instead of reloading:

document.addEventListener('omxus:signin', (e) => { /* e.detail.session */ });
document.addEventListener('omxus:error',  (e) => { /* e.detail.error   */ });

The same events, without the omxus: prefix (signin, error), also bubble from the script tag itself.

What happens on a press

From worker/button.js in the auth repo, which is what auth.omxus.com serves:

  1. On load it imports /engine/engine.bundle.mjs (served) and /sdk/omxus-credential.mjs (404 today) from auth.omxus.com, and makes a credential store for issuer https://auth.omxus.com.
  2. On a press it calls the store's signIn(): a passkey if the device has one, otherwise a new passkey, otherwise a key sealed on this device. See Memory, not storage.
  3. signIn() asks for a challenge and posts the signature to /auth/challenge and /auth/verify on your page's own origin (the store is made with no base path). Your site would need to forward those to auth.omxus.com; the provider does not send CORS headers for /auth/*.
  4. It posts the result to data-verify and fires omxus:signin.

What your endpoint receives

{
  "issuer": "https://auth.omxus.com",
  "session": { "address": "0x…", "ok": true, "returnTo": "/", "tier": "passkey-seed" }
}

session is the answer from /auth/verify plus the address. The provider's session token goes into a cookie on the provider's origin; it is not in the body. So the body is only a claim: anyone can post any address to your endpoint. Do not create a session from it.

/oauth/introspect does not help here. It reads a form-encoded body, needs your client id and secret, and only answers for OIDC access tokens issued to your client; anything else comes back {"active": false}. To check an access token you do hold:

curl -s https://auth.omxus.com/oauth/introspect \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" \
  -d "token=$ACCESS_TOKEN"
# {"active":true,"scope":"openid profile","client_id":"…","token_type":"Bearer","sub":"…","exp":…,"iat":…}

Untested against a live token here; the request and answer shapes are from server/oidc-introspect.js and server/router.js. The answer has sub, not an address.

Where to put it

Not on /login

A login page is a toll gate. Put the button at the moment somebody reaches for the thing they came for — the feature, the upload, the vote. They press once and carry on, on the same page.

One site lost most of 2,000 daily visitors to a login flow in front of a feature people actively wanted. The feature was never the problem.