Better Auth Sign-In With Solana (SIWS) Plugin
better-auth-siws is a specialized plugin designed to integrate Sign-In With Solana (SIWS) functionality into applications utilizing the Better Auth framework. Currently at version 0.1.3, this package provides both server-side and client-side plugins (`siwsPlugin` and `siwsClientPlugin` respectively) along with a utility (`buildSiwsMessage`) to construct the canonical SIWS message. It streamlines the implementation of SIWS by offering `start` and `verify` endpoints, abstracting away much of the cryptographic and session management complexity when paired with Better Auth. The package is actively maintained and extends Better Auth's capabilities to support Solana-based authentication flows, differentiating itself by providing a tightly integrated solution specifically for the Better Auth ecosystem.
Common errors
-
Error: SIWS verification failed: Domain mismatch.
cause The 'domain' in the signed SIWS message does not match the 'domain' configured in the server's `siwsPlugin` instance.fixCheck the `domain` option passed to `siwsPlugin({ domain: '...' })` on your server to ensure it precisely matches the `domain` of your application where the SIWS flow is initiated. -
Error: SIWS verification failed: Nonce expired.
cause The time elapsed between the server issuing a nonce and the client submitting the signed message for verification exceeded the configured `nonceTtlSeconds`.fixExpedite the client-side signing process, or increase the `nonceTtlSeconds` option in your server's `siwsPlugin` configuration (e.g., `siwsPlugin({ nonceTtlSeconds: 600 })`). -
TypeError: wallet.signMessage is not a function
cause The connected Solana wallet object does not expose a `signMessage` method, or it's not available when the SIWS signing attempt is made.fixEnsure a compatible Solana wallet (e.g., via `@solana/wallet-adapter`) is connected and the wallet instance provides the `signMessage` function with the expected signature (`(data: Uint8Array) => Promise<Uint8Array>`). -
Error: Unauthorized origin for SIWS start/verify endpoint.
cause The `Origin` header of the client request is not included in the `trustedOrigins` array configured in your `better-auth` server instance.fixAdd the full origin URL of your frontend application (e.g., `https://app.example.com`) to the `trustedOrigins` array in your `betterAuth({ security: { trustedOrigins: [...] } })` server configuration.
Warnings
- gotcha The 'domain' configured in the `siwsPlugin` on your server MUST precisely match the domain your application is hosted on (without protocol) and align with the 'domain' returned by the `/siws/start` endpoint. A mismatch will cause signature verification to fail due to the SIWS specification.
- gotcha The `nonceTtlSeconds` configured on the server-side `siwsPlugin` (default: 300 seconds) defines how long a generated nonce remains valid. If the user takes too long to sign the message and send it for verification, the nonce may expire, leading to verification failure.
- gotcha The client-side SIWS flow requires a connected Solana wallet that implements a `signMessage` method which accepts a `Uint8Array` message and returns a `Promise<Uint8Array>` signature. Wallets or adapter libraries that provide different APIs for message signing will not work directly.
- gotcha This package is a plugin for `better-auth`. Proper functioning relies on `better-auth` itself being correctly configured, especially its `baseURL` and `security.trustedOrigins`. Requests from unlisted origins will be rejected by Better Auth's security middleware, preventing SIWS flow completion.
Install
-
npm install better-auth-siws -
yarn add better-auth-siws -
pnpm add better-auth-siws
Imports
- siwsPlugin
const siwsPlugin = require('better-auth-siws').siwsPlugin;import { siwsPlugin } from 'better-auth-siws'; - siwsClientPlugin
import { siwsClientPlugin } from 'better-auth-siws';import { siwsClientPlugin } from 'better-auth-siws/client'; - buildSiwsMessage
const buildSiwsMessage = require('better-auth-siws').buildSiwsMessage;import { buildSiwsMessage } from 'better-auth-siws';
Quickstart
import bs58 from "bs58";
import { buildSiwsMessage } from "better-auth-siws";
import { createAuthClient } from "better-auth/client";
import { siwsClientPlugin } from "better-auth-siws/client";
// 1. Initialize Better Auth client with SIWS plugin
export const clientAuth = createAuthClient({
baseURL: "https://app.example.com/api/auth", // Ensure this matches your server's baseURL
plugins: [siwsClientPlugin()],
});
/**
* Simulates a Solana wallet with publicKey and signMessage capabilities.
* In a real app, this would come from a wallet adapter (e.g., @solana/wallet-adapter).
*/
const mockWallet = {
publicKey: { toBase58: () => 'HbC8N9p6jR8g7A6y5X4Z3W2V1U0T9S8Q' },
signMessage: async (data: Uint8Array) => {
// In a real app, this would prompt the user to sign
// For this example, returning a dummy signature
console.log("Wallet signing message:", new TextDecoder().decode(data));
return new Uint8Array(Array(64).fill(0)); // Dummy 64-byte signature
}
};
async function signInWithSolana(wallet: typeof mockWallet) {
// 2. Request a nonce from the server
const address = wallet.publicKey.toBase58();
const { nonce, domain, uri } = await clientAuth.api.start({
body: { address },
});
// 3. Build the canonical SIWS message for signing
const message = buildSiwsMessage({
domain,
address,
uri,
nonce,
issuedAt: new Date().toISOString(),
});
// 4. Have the user's wallet sign the message
const signatureBytes = await wallet.signMessage(new TextEncoder().encode(message));
const signature = bs58.encode(signatureBytes);
// 5. Send signed message and signature to the server for verification
const result = await clientAuth.api.verify({
body: { address, message, signature },
});
console.log("Sign-In successful!");
console.log("User session:", result);
return result;
}
// Example usage:
signInWithSolana(mockWallet).catch(console.error);