From ca34356adda403393126ab75c69a6067eeb8a818 Mon Sep 17 00:00:00 2001 From: Wonhee Lee <2wheeh@gmail.com> Date: Thu, 7 May 2026 14:36:11 +0900 Subject: [PATCH 1/4] feat: make getNonce optional in AuthenticationAdapter --- .../AuthenticationContext.tsx | 30 +++++++++++++----- .../src/components/SignIn/SignIn.tsx | 31 +++++++++++++------ 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/packages/rainbowkit/src/components/RainbowKitProvider/AuthenticationContext.tsx b/packages/rainbowkit/src/components/RainbowKitProvider/AuthenticationContext.tsx index dbc5655d83..3ef3ed8662 100644 --- a/packages/rainbowkit/src/components/RainbowKitProvider/AuthenticationContext.tsx +++ b/packages/rainbowkit/src/components/RainbowKitProvider/AuthenticationContext.tsx @@ -14,17 +14,33 @@ export type AuthenticationStatus = | 'unauthenticated' | 'authenticated'; -export interface AuthenticationAdapter { - getNonce: () => Promise; - createMessage: (args: { - nonce: string; - address: Address; - chainId: number; - }) => Promise | Message; +interface AuthenticationAdapterBase { verify: (args: { message: Message; signature: string }) => Promise; signOut: () => Promise; } +// When `getNonce` is omitted, RainbowKit skips client-side nonce pre-fetching +// and calls `createMessage` directly with `{ address, chainId }` +export type AuthenticationAdapter = + AuthenticationAdapterBase & + ( + | { + getNonce: () => Promise; + createMessage: (args: { + nonce: string; + address: Address; + chainId: number; + }) => Promise | Message; + } + | { + getNonce?: undefined; + createMessage: (args: { + address: Address; + chainId: number; + }) => Promise | Message; + } + ); + export interface AuthenticationConfig { adapter: AuthenticationAdapter; status: AuthenticationStatus; diff --git a/packages/rainbowkit/src/components/SignIn/SignIn.tsx b/packages/rainbowkit/src/components/SignIn/SignIn.tsx index 993ced248a..ce82eef184 100644 --- a/packages/rainbowkit/src/components/SignIn/SignIn.tsx +++ b/packages/rainbowkit/src/components/SignIn/SignIn.tsx @@ -30,6 +30,8 @@ export function SignIn({ const authAdapter = useAuthenticationAdapter(); const getNonce = useCallback(async () => { + if (!authAdapter.getNonce) return; + try { const nonce = await authAdapter.getNonce(); setState((x) => ({ ...x, nonce })); @@ -62,9 +64,10 @@ export function SignIn({ const chainId = activeChain?.id; const { nonce } = state; - if (!address || !chainId || !nonce) { - return; - } + if (!address || !chainId) return; + + // Block when the adapter expects a pre-fetched nonce that hasn't arrived. + if (authAdapter.getNonce && !nonce) return; setState((x) => ({ ...x, @@ -73,11 +76,18 @@ export function SignIn({ let message: Awaited>; try { - const messageResult = authAdapter.createMessage({ - address, - chainId, - nonce, - }); + let messageResult: ReturnType; + if (authAdapter.getNonce) { + // This isn't reachable in practice, but for type safety. + if (!nonce) return; + messageResult = authAdapter.createMessage({ + address, + chainId, + nonce, + }); + } else { + messageResult = authAdapter.createMessage({ address, chainId }); + } if (messageResult instanceof Promise) { setState((x) => ({ ...x, status: 'creatingMessage' })); @@ -223,13 +233,14 @@ export function SignIn({ > Date: Thu, 7 May 2026 14:36:43 +0900 Subject: [PATCH 2/4] test: for optional getNonce path --- .../src/components/SignIn/SignIn.test.tsx | 60 ++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/packages/rainbowkit/src/components/SignIn/SignIn.test.tsx b/packages/rainbowkit/src/components/SignIn/SignIn.test.tsx index 19cd92e609..0a5ba62ca5 100644 --- a/packages/rainbowkit/src/components/SignIn/SignIn.test.tsx +++ b/packages/rainbowkit/src/components/SignIn/SignIn.test.tsx @@ -32,19 +32,34 @@ const address = '0x0000000000000000000000000000000000000001' as Address; function renderSignIn({ createMessage, + getNonce = async () => 'nonce', }: { createMessage: (args: { - nonce: string; + nonce?: string; address: Address; chainId: number; }) => string | Promise; + // Pass `null` to omit `getNonce` entirely (server-side message creation). + getNonce?: (() => Promise) | null; }) { - const adapter = createAuthenticationAdapter({ - createMessage, - getNonce: async () => 'nonce', - signOut: async () => {}, - verify: async () => true, - }); + const adapter = createAuthenticationAdapter( + getNonce === null + ? { + createMessage, + signOut: async () => {}, + verify: async () => true, + } + : { + createMessage: createMessage as (args: { + nonce: string; + address: Address; + chainId: number; + }) => string | Promise, + getNonce, + signOut: async () => {}, + verify: async () => true, + }, + ); return render( { fireEvent.click(button); expect(createMessage).toHaveBeenCalledOnce(); + expect(createMessage).toHaveBeenCalledWith({ + address, + chainId: 1, + nonce: 'nonce', + }); await waitFor(() => expect(wagmiMocks.signMessageAsync).toHaveBeenCalledWith({ message: 'message', @@ -142,4 +162,30 @@ describe('SignIn', () => { expect(createMessage).toHaveBeenCalledTimes(2); }); + + it('enables sign-in immediately when getNonce is omitted', async () => { + const createMessage = vi.fn(() => 'message'); + wagmiMocks.signMessageAsync.mockReturnValue(new Promise(() => {})); + + renderSignIn({ createMessage, getNonce: null }); + + const button = await screen.findByTestId('rk-auth-message-button'); + expect(button).toHaveTextContent('Sign message'); + expect(button).not.toBeDisabled(); + + fireEvent.click(button); + + expect(createMessage).toHaveBeenCalledOnce(); + // No nonce should be passed to createMessage when getNonce is omitted. + expect(createMessage).toHaveBeenCalledWith({ + address, + chainId: 1, + }); + + await waitFor(() => + expect(wagmiMocks.signMessageAsync).toHaveBeenCalledWith({ + message: 'message', + }), + ); + }); }); From 50002d0bad04bb83279bba5b626764e5f193e6e1 Mon Sep 17 00:00:00 2001 From: Wonhee Lee <2wheeh@gmail.com> Date: Thu, 7 May 2026 14:37:26 +0900 Subject: [PATCH 3/4] docs: update Server-side message creation for optional getNonce --- site/data/en-US/docs/custom-authentication.mdx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/site/data/en-US/docs/custom-authentication.mdx b/site/data/en-US/docs/custom-authentication.mdx index 0c7f0e4547..8d7da505a7 100644 --- a/site/data/en-US/docs/custom-authentication.mdx +++ b/site/data/en-US/docs/custom-authentication.mdx @@ -57,7 +57,10 @@ const authenticationAdapter = createAuthenticationAdapter({ `createMessage` also supports returning a `Promise`, allowing you to construct [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361) messages on the server. For stricter security, the server endpoint should derive or validate security-critical fields like `domain`, `nonce`, and `issued-at` timestamps instead of trusting client-provided values. See [SIWE documentation](https://docs.siwe.xyz/security-considerations#server-side-message-generation) for more details. +When the server generates the nonce alongside the message, you can omit `getNonce`. RainbowKit will skip nonce pre-fetching and call `createMessage` directly with `{ address, chainId }`: + ```tsx +const authenticationAdapter = createAuthenticationAdapter({ createMessage: async ({ address, chainId }) => { const response = await fetch('/api/siwe/message', { method: 'POST', @@ -71,8 +74,19 @@ const authenticationAdapter = createAuthenticationAdapter({ return await response.text(); }, + + verify: async ({ message, signature }) => { + /* ... */ + }, + + signOut: async () => { + /* ... */ + }, +}); ``` +If you still want the client to fetch a nonce separately (for example, to keep the existing `/api/nonce` endpoint), keep `getNonce` and continue to receive `nonce` in `createMessage` args. + #### Providing the authentication state Assuming your application is already managing the authentication lifecycle in some way, you can pass the current authentication status along with your custom adapter to `RainbowKitAuthenticationProvider`, wrapping your existing `RainbowKitProvider`. From df43ae194dd85fed16534e07ce990ab5eecc246d Mon Sep 17 00:00:00 2001 From: Wonhee Lee <2wheeh@gmail.com> Date: Thu, 7 May 2026 14:38:38 +0900 Subject: [PATCH 4/4] chore: changeset --- .changeset/poor-ends-peel.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/poor-ends-peel.md diff --git a/.changeset/poor-ends-peel.md b/.changeset/poor-ends-peel.md new file mode 100644 index 0000000000..c3262eaf6c --- /dev/null +++ b/.changeset/poor-ends-peel.md @@ -0,0 +1,5 @@ +--- +"@rainbow-me/rainbowkit": patch +--- + +Made `getNonce` optional in `AuthenticationAdapter`. When the server generates the nonce alongside the SIWE message, you can now omit `getNonce` entirely—RainbowKit will skip the client-side nonce pre-fetch and call `createMessage` directly with `{ address, chainId }`. Adapters that already provide `getNonce` keep the existing behavior and continue to receive `nonce` in `createMessage` args.