Existing Customers: Login Token Migration
Required backend change for sites that integrated Key-Rex before the signed login token was introduced.
New to Key-Rex? You don't need this page. The main documentation already shows the correct integration.
Why this change is required
The identifier (xpub) is derived from a public key. It is not a secret and it is the same on every Key-Rex site. If your backend creates a session from a bare xpub, anyone who knows a user's identifier can present it to your backend and be logged in as that user.
Key-Rex now sends a short-lived signed login token with every login. Your backend must verify it instead of trusting the raw identifier. Raw identifier delivery will be removed on the sunset date announced in your dashboard, after which un-migrated sites will no longer receive an identifier.
Is my site affected?
You are affected if any of these are true:
- Your
onSuccesshandler sendsxpubto your backend - Your backend has an endpoint that accepts
xpuband starts a session - You read
?xpub=from the URL after a mobile redirect
Your users do not need to do anything. They keep the same wallet and the same accounts.
Migration steps
- Create a nonce per login attempt on your backend (at least 128 bits of randomness, characters
A-Z a-z 0-9 _ -, 16 to 128 long). Store it in the visitor's pre-login session and setwindow.KeyRex.noncebefore the user clicks login. - Send the token, not the xpub.
onSuccess(xpub, token)now provides a signed JWT as the second argument. Post only thetokento your backend. - Verify the token with a standard JOSE library and Key-Rex's public keys at
/.well-known/jwks.json(cache them). Check: ES256 only;issis the Key-Rex base URL;audis your own siteId; not expired;noncematches the one you stored (then discard it);jtinot seen before. - Map existing users. During the transition the token also carries
legacy_sub(the old xpub). Look up existing accounts bylegacy_sub, then save the token'ssubon the account so future logins match bysub. - Create the session only after steps 3 and 4 succeed.
- Remove the old code. Delete any endpoint that accepts a bare
xpuband starts a session, and stop reading?xpub=from the URL. - Test that a bare identifier, a token for another site, an expired token, a wrong nonce and a replayed token are all rejected.
- Confirm in your Key-Rex dashboard that your site no longer shows legacy-flow logins.
Before and after
Before (insecure, remove)
window.KeyRex.onSuccess = function(xpub) {
fetch('/api/auth/callback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ xpub: xpub }) // anyone can send this
});
};
// backend
app.post('/api/auth/callback', (req, res) => {
const user = findOrCreateUser(req.body.xpub); // trusts a public value
req.session.userId = user.id;
});After: frontend
// Ask YOUR backend for a fresh nonce (it stores it in the visitor's session)
fetch('/api/keyrex/nonce', { method: 'POST' })
.then(r => r.json())
.then(({ nonce }) => { window.KeyRex.nonce = nonce; });
window.KeyRex.onSuccess = function(xpub, token) {
fetch('/api/auth/keyrex', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: token })
});
};After: backend (Node.js, jose)
import crypto from 'crypto';
import { createRemoteJWKSet, jwtVerify } from 'jose';
const KEYREX = process.env.KEYREX_BASE_URL; // e.g. https://key-rex.com
const SITE_ID = process.env.KEYREX_SITE_ID; // your own siteId
const jwks = createRemoteJWKSet(new URL(KEYREX + '/.well-known/jwks.json'));
app.post('/api/keyrex/nonce', (req, res) => {
req.session.keyrexNonce = crypto.randomBytes(24).toString('base64url');
res.json({ nonce: req.session.keyrexNonce });
});
app.post('/api/auth/keyrex', async (req, res) => {
try {
const expected = req.session.keyrexNonce;
req.session.keyrexNonce = null; // single use
const { payload } = await jwtVerify(req.body.token, jwks, {
issuer: KEYREX,
audience: SITE_ID,
algorithms: ['ES256'],
clockTolerance: 5,
});
if (!expected || payload.nonce !== expected) throw new Error('nonce mismatch');
if (await seenJti(payload.jti)) throw new Error('token replayed');
await rememberJti(payload.jti, payload.exp);
// Existing users: match by legacy_sub once, then save sub on the account
const user = await findOrCreateUserBySub(payload.sub, payload.legacy_sub);
req.session.userId = user.id;
res.json({ success: true });
} catch (e) {
res.status(401).json({ error: 'Invalid login token' });
}
});Extra checks for existing deployments
- Review logs for logins that do not match real wallet activity. A past forgery would show a login with no corresponding Key-Rex login.
- Invalidate existing sessions only if you suspect abuse.
- Never log tokens, and keep them out of URLs where possible.
- Keep your database column for the old xpub until the sunset date, then it can be dropped once every account has a
sub.