Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/poor-ends-peel.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,33 @@ export type AuthenticationStatus =
| 'unauthenticated'
| 'authenticated';

export interface AuthenticationAdapter<Message> {
getNonce: () => Promise<string>;
createMessage: (args: {
nonce: string;
address: Address;
chainId: number;
}) => Promise<Message> | Message;
interface AuthenticationAdapterBase<Message> {
verify: (args: { message: Message; signature: string }) => Promise<boolean>;
signOut: () => Promise<void>;
}

// When `getNonce` is omitted, RainbowKit skips client-side nonce pre-fetching
// and calls `createMessage` directly with `{ address, chainId }`
export type AuthenticationAdapter<Message> =
AuthenticationAdapterBase<Message> &
(
| {
getNonce: () => Promise<string>;
createMessage: (args: {
nonce: string;
address: Address;
chainId: number;
}) => Promise<Message> | Message;
}
| {
getNonce?: undefined;
createMessage: (args: {
address: Address;
chainId: number;
}) => Promise<Message> | Message;
}
);

export interface AuthenticationConfig<Message> {
adapter: AuthenticationAdapter<Message>;
status: AuthenticationStatus;
Expand Down
60 changes: 53 additions & 7 deletions packages/rainbowkit/src/components/SignIn/SignIn.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
// Pass `null` to omit `getNonce` entirely (server-side message creation).
getNonce?: (() => Promise<string>) | null;
}) {
const adapter = createAuthenticationAdapter<string>({
createMessage,
getNonce: async () => 'nonce',
signOut: async () => {},
verify: async () => true,
});
const adapter = createAuthenticationAdapter<string>(
getNonce === null
? {
createMessage,
signOut: async () => {},
verify: async () => true,
}
: {
createMessage: createMessage as (args: {
nonce: string;
address: Address;
chainId: number;
}) => string | Promise<string>,
getNonce,
signOut: async () => {},
verify: async () => true,
},
);

return render(
<RainbowKitAuthenticationProvider
Expand Down Expand Up @@ -78,6 +93,11 @@ describe('SignIn', () => {
fireEvent.click(button);

expect(createMessage).toHaveBeenCalledOnce();
expect(createMessage).toHaveBeenCalledWith({
address,
chainId: 1,
nonce: 'nonce',
});
await waitFor(() =>
expect(wagmiMocks.signMessageAsync).toHaveBeenCalledWith({
message: 'message',
Expand Down Expand Up @@ -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',
}),
);
});
});
31 changes: 21 additions & 10 deletions packages/rainbowkit/src/components/SignIn/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
Expand Down Expand Up @@ -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,
Expand All @@ -73,11 +76,18 @@ export function SignIn({

let message: Awaited<ReturnType<typeof authAdapter.createMessage>>;
try {
const messageResult = authAdapter.createMessage({
address,
chainId,
nonce,
});
let messageResult: ReturnType<typeof authAdapter.createMessage>;
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' }));
Expand Down Expand Up @@ -223,13 +233,14 @@ export function SignIn({
>
<ActionButton
disabled={
!state.nonce ||
(authAdapter.getNonce && !state.nonce) ||
status === 'creatingMessage' ||
status === 'signing' ||
status === 'verifying'
}
label={
!state.nonce || status === 'creatingMessage'
(authAdapter.getNonce && !state.nonce) ||
status === 'creatingMessage'
? i18n.t('sign_in.message.preparing')
: status === 'signing'
? i18n.t('sign_in.signature.waiting')
Expand Down
14 changes: 14 additions & 0 deletions site/data/en-US/docs/custom-authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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`.
Expand Down