Sign In With Hardware Wallet

nextjs-hardware-signin
Sign In With a Hardware Wallet — wallet authentication for hardware wallets that cannot sign arbitrary off-chain messages. Built on @solana/kit v7 with the kit plugin client and @solana/react.
Instead of signMessage (unsupported by many hardware wallets), the wallet signs a memo transaction carrying a server-issued nonce. The server verifies the Ed25519 signature off-chain — the transaction is never broadcast.
Getting Started
npx -y create-solana-dapp@latest -t solana-foundation/templates/kit/nextjs-hardware-signinnpm install
npm run devOpen http://localhost:3000, connect a wallet, and click Sign in. Approve the signature in your wallet — nothing hits the chain.
Why a memo transaction?
Many hardware wallets do not support signing arbitrary messages, so the usual Sign-In-With-Solana signMessage flow does not work for those users. The workaround, first suggested here, is to have the wallet sign a transaction whose only instruction is a memo containing a nonce, then verify that signature server-side without ever sending the transaction.
Flow
Nonce — the client requests a single-use nonce from
GET /api/nonce.Sign — the client builds a memo transaction with the nonce and signs it, without sending:
const message = await client.memo.instructions .addMemo({ memo: nonce, signers: [signer] }) .planTransaction(); // builds an unsigned message — does NOT send const signedTx = await signTransactionMessageWithSigners(message);Verify — the client posts
{ address, messageBytesBase64, signatureBase64, nonce }toPOST /api/session. The server (app/lib/auth.ts) checks that:- the Ed25519 signature is valid over the exact signed bytes (
getPublicKeyFromAddress+verifySignature), - those bytes decode to a memo instruction whose text equals the nonce, and
- the transaction fee payer is the claimed signer.
On success it consumes the nonce (single use) and sets an
httpOnlysession cookie.- the Ed25519 signature is valid over the exact signed bytes (
What's Included
- Wallet connection via
@solana/kit-plugin-wallet(wallet-standard discovery, auto-reconnect) - Sign-in flow with off-chain signature verification — no transaction sent
- Route handlers for nonce issuance and session management (
app/api) - Server-gated dashboard (
app/protected) — a restricted page that redirects unauthenticated visitors home - Network switcher — devnet, testnet, mainnet, localnet
- Offline tests with
vitest— the sign-and-verify flow needs no RPC or validator - Tailwind CSS v4 with light/dark mode
The kit client
The app builds one kit client per selected cluster in app/lib/solana-client.ts:
createClient()
.use(walletSigner({ chain })) // wallet as signer + identity; must precede rpc
.use(solanaRpc({ rpcUrl, rpcSubscriptionsUrl })) // rpc + transaction planner
.use(memoProgram()); // client.memo.instructions.addMemoProtected route
/protected (app/protected/page.tsx) is a server component that reads the session cookie and calls readSessionToken — a forged or missing cookie is redirected home before anything renders. The authorization check runs in the Node runtime (HMAC verification), so gating lives in the server component rather than edge middleware. Signing in redirects here.
Testing
Because the transaction is never broadcast, the entire sign-and-verify flow runs offline — no RPC, validator, or funded account. The test builds a memo transaction message by hand, signs it with a generated keypair, and asserts the server verifier (verifyAuthProof) accepts a valid proof and rejects a wrong nonce or tampered signature:
npm run testSee test/hardware-signin.test.ts.
Production notes
This template favors clarity over completeness. For real deployments:
- The nonce store (
app/lib/nonce-store.ts) is in-memory and per-process — back it with Redis or a database across instances. - The session cookie is HMAC-signed (
app/lib/auth.ts) so its address cannot be forged. SetSESSION_SECRETin the environment; it is required in production, where signing or verifying a token without it throws. The insecure fallback applies only outside production. For richer sessions (expiry, claims) reach for a signed JWT.
