@wardauth/sdk-js
v2.1.0
Published
Official JavaScript SDK for WardAuth — passwordless authentication as a service
Maintainers
Readme
@wardauth/sdk-js
Official JavaScript SDK for WardAuth — passwordless authentication as a service.
Works in browsers (Chrome 109+, Firefox 130+, Safari 17+) and Node.js 20+.
Install
npm install @wardauth/sdk-jsQuick start
import { WardAuth } from "@wardauth/sdk-js";
const auth = new WardAuth({
baseUrl: "https://api.wardauth.com",
apiKey: "wardauth_live_...",
});Authentication methods
Email + Password (zero-knowledge)
The password never leaves the device. It's used to derive an Ed25519 key pair locally via PBKDF2 (600k iterations). The server only sees the public key.
Same email + same password = same key pair on any device. No password sync needed.
// Register
await auth.registerWithPassword({
email: "[email protected]",
password: "my-strong-password-123", // min 12 chars, never sent to server
});
// Sign in (any device — same password = same key)
const session = await auth.signInWithPassword(
"[email protected]",
"my-strong-password-123"
);
console.log(session.user.sub); // "[email protected]"
console.log(session.accessToken);Passwordless (Ed25519 key pair)
For apps that don't want passwords at all. A random Ed25519 key pair is generated and stored on the device.
// Register (generates key pair automatically)
const reg = await auth.register({ email: "[email protected]" });
console.log(reg.created); // true on first registration
// Sign in (requires the same device or key sync)
const session = await auth.signIn("[email protected]");Magic link (email)
No local key pair required. The server creates the user on first sendMagicLink if that email is new.
Configure outbound email and magic_link_redirect_url / app_url in tenant settings so the email link lands on your app.
await auth.sendMagicLink("[email protected]");
// Callback page (query string from the redirect):
const params = new URLSearchParams(window.location.search);
const session = await auth.verifyMagicLink(
params.get("token")!,
params.get("tenant")!
);Sign out
await auth.signOut();
// Revokes refresh token on server + clears local session and keysReact
import { WardAuthProvider, useAuth } from "@wardauth/sdk-js/react";
function App() {
return (
<WardAuthProvider
baseUrl="https://api.wardauth.com"
apiKey="wardauth_live_..."
>
<LoginPage />
</WardAuthProvider>
);
}
function LoginPage() {
const { signIn, signOut, user, loading } = useAuth();
if (loading) return <p>Loading...</p>;
if (user) return (
<div>
<p>Signed in as {user.sub}</p>
<button onClick={signOut}>Sign out</button>
</div>
);
return (
<button onClick={() => signIn("[email protected]")}>
Sign in
</button>
);
}Route guard
import { useRequireAuth } from "@wardauth/sdk-js/react";
function Dashboard() {
const { ready, user } = useRequireAuth("/login");
if (!ready) return <p>Redirecting...</p>;
return <h1>Welcome, {user?.sub}</h1>;
}MFA hook
import { useMfa } from "@wardauth/sdk-js/react";
function MfaSettings() {
const { factors, enrollTotp, revokeFactor, loading } = useMfa();
// ...
}MFA — TOTP
// Enroll
const { qrCode, factorId } = await auth.mfa.enrollTotp();
// Show qrCode (data:image/png;base64,...) to the user
// Verify with the first code from their authenticator app
const { recoveryCodes } = await auth.mfa.verifyTotpEnrollment(factorId, "123456");
// Show recoveryCodes — displayed only once
// Verify at login time
await auth.mfa.verifyTotp("123456");MFA — WebAuthn / Passkey
import { startRegistration, startAuthentication } from "@simplewebauthn/browser";
// Register passkey
const options = await auth.mfa.beginPasskeyRegistration("MacBook Pro");
const response = await startRegistration(options);
await auth.mfa.completePasskeyRegistration(response, "MacBook Pro");
// Authenticate with passkey
const authOpts = await auth.mfa.beginPasskeyAuthentication();
const authResp = await startAuthentication(authOpts);
await auth.mfa.completePasskeyAuthentication(authResp);MFA — Recovery codes
const { verified, remainingCodes } = await auth.mfa.verifyRecoveryCode("ABCD-EFGH-IJKL");MFA — Revoke a factor
await auth.mfa.revokeFactor(factorId);Multi-device (passwordless mode)
// Must be signed in first.
// On a new device: generate a new key pair and attach it to the authenticated account
const { publicKey } = await auth.addDevice();
// List registered keys for current user
const keys = await auth.listDevices();
// Remove one key by id
await auth.removeDevice(keys[0].id);Not needed for password mode — the key pair is derived from the password.
Node.js / Backend usage
import { WardAuth, memoryAdapter } from "@wardauth/sdk-js";
const auth = new WardAuth({
baseUrl: "https://api.wardauth.com",
apiKey: "wardauth_live_...",
storage: memoryAdapter(), // no localStorage in Node.js
});
// Same API as browser
const session = await auth.signInWithPassword("[email protected]", "password");Custom storage adapter
// React Native with AsyncStorage
const auth = new WardAuth({
baseUrl: "...",
apiKey: "...",
storage: {
get: (key) => AsyncStorage.getItem(key),
set: (key, val) => AsyncStorage.setItem(key, val),
remove: (key) => AsyncStorage.removeItem(key),
},
});Built-in adapters:
localStorageAdapter— browser defaultsessionStorageAdapter— cleared on tab close (more secure)memoryAdapter()— in-memory, for SSR / Node.js / tests
Auth state changes
const unsub = auth.onAuthStateChange(event => {
switch (event.type) {
case "signed_in": console.log("Signed in", event.session.user.sub); break;
case "signed_out": console.log("Signed out"); break;
case "token_refreshed": console.log("Token refreshed"); break;
case "error": console.error(event.error); break;
}
});
unsub(); // unsubscribeSession management
// Get current session (auto-refreshes if needed)
const session = await auth.getSession();
// Get current user
const user = await auth.getUser();
// Get stored public key
const pubKey = await auth.getPublicKey();REST API reference
All SDK methods map to these REST endpoints:
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /.well-known/jwks.json | Public keys for access JWT verification (EdDSA) |
| GET | /v1/auth/salt?email=... | Get derivation salt for password mode |
| POST | /v1/register | Register account (idempotent for same key; conflict for different key on existing account) |
| POST | /v1/magic-link/send | Send magic link email (creates user row if new) |
| GET | /v1/magic-link/verify?token=...&tenant=... | Verify one-time token → JWT tokens |
| POST | /v1/challenge | Request auth challenge (nonce) |
| POST | /v1/challenge/verify | Verify signature, get JWT tokens |
| POST | /v1/token/refresh | Refresh access token |
| POST | /v1/token/revoke | Revoke refresh token (logout) |
| POST | /v1/keys | Add a key for authenticated user |
| GET | /v1/keys | List keys for authenticated user |
| DELETE | /v1/keys/:id | Remove a key for authenticated user |
| POST | /v1/mfa/totp/enroll | Start TOTP enrollment |
| POST | /v1/mfa/totp/enroll/verify | Confirm TOTP enrollment |
| POST | /v1/mfa/totp/verify | Verify TOTP code at login |
| POST | /v1/mfa/webauthn/register/begin | Start passkey registration |
| POST | /v1/mfa/webauthn/register/complete | Complete passkey registration |
| POST | /v1/mfa/webauthn/authenticate/begin | Start passkey authentication |
| POST | /v1/mfa/webauthn/authenticate/complete | Complete passkey authentication |
| POST | /v1/mfa/recovery/verify | Use a recovery code |
| GET | /v1/mfa/factors | List MFA factors |
| DELETE | /v1/mfa/factors/:id | Revoke an MFA factor |
Send your tenant API key as X-API-Key or Authorization: Bearer <wardauth_...> (this SDK uses Bearer on public calls).
Authenticated user endpoints use X-API-Key plus Authorization: Bearer <access_token>.
Register semantics
- First registration for an email:
201withcreated: true. - Retry with same email + same key: success with
created: false(idempotent). - Existing account with different key:
409 CONFLICT(ACCOUNT_EXISTS_USE_LOGINpattern).
Security model
- Password mode: Password never leaves the device. PBKDF2 (600k iterations) derives an Ed25519 seed. Server stores only the public key.
- Passwordless mode: Random Ed25519 key pair generated on device. Private key stored locally.
- Both modes: Authentication is challenge-response (server sends nonce, client signs with private key, server verifies with public key).
- Magic link: One-time emailed token (~10 min, single use); same JWT pair as challenge verify, without a device key pair.
- Tokens: Access JWTs are EdDSA (Ed25519) — verify on your backend with
WARDAUTH_JWKS_URL(public/.well-known/jwks.jsonon the auth host) or a staticWARDAUTH_JWT_PUBLIC_JWK. Refresh tokens stay HS256 (internal to WardAuth). This SDK only decodes the access JWT in the browser for UX (sub,exp); it does not verify signatures. - MFA: TOTP, WebAuthn/Passkeys, and recovery codes. All optional, configurable per tenant.
License
MIT
