Skip to content

Commit 737a1d6

Browse files
feat: add authentication and SIWE/NextAuth support (rainbow-me#636)
1 parent 488c5a1 commit 737a1d6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+2754
-372
lines changed

.changeset/gentle-mayflies-fetch.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@rainbow-me/rainbowkit-siwe-next-auth': minor
3+
---
4+
5+
Initial release.

.changeset/great-mangos-hammer.md

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
'@rainbow-me/rainbowkit': minor
3+
---
4+
5+
Added support for authentication.
6+
7+
RainbowKit now provides first-class support for [Sign-In with Ethereum](https://login.xyz) and [NextAuth.js](https://next-auth.js.org) via the `@rainbow-me/rainbowkit-siwe-next-auth` package, as well as lower-level APIs for integrating with custom back-ends and message formats.
8+
9+
For more information on how to integrate this feature into your application, check out the full [RainbowKit authentication guide.](https://www.rainbowkit.com/docs/authentication)
10+
11+
**Migration guide for custom ConnectButton implementations**
12+
13+
If you're using `ConnectButton.Custom` and want to make use of authentication, you'll want to update the logic in your render prop to use the new `authenticationStatus` property, which is either `"loading"` (during initial page load), `"unauthenticated"` or `"authenticated"`.
14+
15+
For example, if you wanted to display the "Connect Wallet" state when the user has connected their wallet but haven't authenticated, you can calculate the state in the following way:
16+
17+
```tsx
18+
<ConnectButton.Custom>
19+
{({
20+
account,
21+
chain,
22+
openAccountModal,
23+
openChainModal,
24+
openConnectModal,
25+
authenticationStatus,
26+
mounted,
27+
}) => {
28+
const ready = mounted && authenticationStatus !== 'loading';
29+
const connected =
30+
ready &&
31+
account &&
32+
chain &&
33+
(!authenticationStatus || authenticationStatus === 'authenticated');
34+
35+
return (
36+
<div
37+
{...(!ready && {
38+
'aria-hidden': true,
39+
'style': {
40+
opacity: 0,
41+
},
42+
})}
43+
>
44+
{/* etc... */}
45+
</div>
46+
);
47+
}}
48+
</ConnectButton.Custom>
49+
```
50+
51+
For a more complete example and API documentation, check out the [custom ConnectButton documentation.](https://www.rainbowkit.com/docs/custom-connect-button)

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ The following examples are provided in the [examples](./examples/) folder of thi
4444
- `with-next`
4545
- `with-next-custom-button`
4646
- `with-next-mint-nft`
47+
- `with-next-siwe-next-auth`
48+
- `with-next-siwe-iron-session`
4749
- `with-remix`
4850

4951
### Running examples
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
NEXT_IRON_PASSWORD= # Mac/Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
2+
3+
NEXT_PUBLIC_ENABLE_TESTNETS=false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "next/core-web-vitals"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
.pnpm-debug.log*
27+
28+
# local env files
29+
.env.local
30+
.env.development.local
31+
.env.test.local
32+
.env.production.local
33+
34+
# vercel
35+
.vercel
36+
37+
# typescript
38+
*.tsbuildinfo
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2+
3+
## Getting Started
4+
5+
First, run the development server:
6+
7+
```bash
8+
npm run dev
9+
# or
10+
yarn dev
11+
```
12+
13+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
14+
15+
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
16+
17+
## Learn More
18+
19+
To learn more about Next.js, take a look at the following resources:
20+
21+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
22+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
23+
24+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
25+
26+
## Deploy on Vercel
27+
28+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
29+
30+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { IronSessionOptions } from 'iron-session';
2+
3+
export const ironOptions: IronSessionOptions = {
4+
cookieName: 'siwe',
5+
cookieOptions: {
6+
secure: process.env.NODE_ENV === 'production',
7+
},
8+
password: process.env.NEXT_IRON_PASSWORD as string,
9+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/image-types/global" />
3+
4+
// NOTE: This file should not be edited
5+
// see https://nextjs.org/docs/basic-features/typescript for more information.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/** @type {import('next').NextConfig} */
2+
const nextConfig = {
3+
reactStrictMode: true,
4+
};
5+
6+
module.exports = nextConfig;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "with-next-siwe-iron-session",
3+
"private": true,
4+
"version": "0.0.0",
5+
"scripts": {
6+
"dev": "next dev",
7+
"build": "next build",
8+
"start": "next start",
9+
"lint": "next lint"
10+
},
11+
"dependencies": {
12+
"@rainbow-me/rainbowkit": "workspace:*",
13+
"ethers": "^5.0.0",
14+
"iron-session": "^6.1.3",
15+
"next": "^12.1.6",
16+
"react": "^18.1.0",
17+
"react-dom": "^18.1.0",
18+
"siwe": "^1.1.6",
19+
"wagmi": "^0.6.0"
20+
},
21+
"devDependencies": {
22+
"@types/node": "^17.0.35",
23+
"@types/react": "^18.0.9",
24+
"eslint": "^8.15.0",
25+
"eslint-config-next": "^12.1.6",
26+
"typescript": "^4.7.2"
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// This example is based on the wagmi SIWE tutorial
2+
// https://wagmi.sh/examples/sign-in-with-ethereum
3+
import '../styles/global.css';
4+
import '@rainbow-me/rainbowkit/styles.css';
5+
import type { AppProps } from 'next/app';
6+
import {
7+
RainbowKitProvider,
8+
getDefaultWallets,
9+
connectorsForWallets,
10+
wallet,
11+
createAuthenticationAdapter,
12+
RainbowKitAuthenticationProvider,
13+
AuthenticationStatus,
14+
} from '@rainbow-me/rainbowkit';
15+
import { chain, configureChains, createClient, WagmiConfig } from 'wagmi';
16+
import { alchemyProvider } from 'wagmi/providers/alchemy';
17+
import { publicProvider } from 'wagmi/providers/public';
18+
import { SiweMessage } from 'siwe';
19+
import { useEffect, useMemo, useRef, useState } from 'react';
20+
21+
const { chains, provider, webSocketProvider } = configureChains(
22+
[
23+
chain.mainnet,
24+
chain.polygon,
25+
chain.optimism,
26+
chain.arbitrum,
27+
...(process.env.NEXT_PUBLIC_ENABLE_TESTNETS === 'true'
28+
? [chain.goerli, chain.kovan, chain.rinkeby, chain.ropsten]
29+
: []),
30+
],
31+
[
32+
alchemyProvider({ apiKey: '_gg7wSSi0KMBsdKnGVfHDueq6xMB9EkC' }),
33+
publicProvider(),
34+
]
35+
);
36+
37+
const { wallets } = getDefaultWallets({
38+
appName: 'RainbowKit demo',
39+
chains,
40+
});
41+
42+
const demoAppInfo = {
43+
appName: 'Rainbowkit Demo',
44+
};
45+
46+
const connectors = connectorsForWallets([
47+
...wallets,
48+
{
49+
groupName: 'Other',
50+
wallets: [
51+
wallet.argent({ chains }),
52+
wallet.trust({ chains }),
53+
wallet.ledger({ chains }),
54+
],
55+
},
56+
]);
57+
58+
const wagmiClient = createClient({
59+
autoConnect: true,
60+
connectors,
61+
provider,
62+
webSocketProvider,
63+
});
64+
65+
export default function App({ Component, pageProps }: AppProps) {
66+
const fetchingStatusRef = useRef(false);
67+
const verifyingRef = useRef(false);
68+
const [authStatus, setAuthStatus] = useState<AuthenticationStatus>('loading');
69+
70+
// Fetch user when:
71+
useEffect(() => {
72+
const fetchStatus = async () => {
73+
if (fetchingStatusRef.current || verifyingRef.current) {
74+
return;
75+
}
76+
77+
fetchingStatusRef.current = true;
78+
79+
try {
80+
const response = await fetch('/api/me');
81+
const json = await response.json();
82+
setAuthStatus(json.address ? 'authenticated' : 'unauthenticated');
83+
} catch (_error) {
84+
setAuthStatus('unauthenticated');
85+
} finally {
86+
fetchingStatusRef.current = false;
87+
}
88+
};
89+
90+
// 1. page loads
91+
fetchStatus();
92+
93+
// 2. window is focused (in case user logs out of another window)
94+
window.addEventListener('focus', fetchStatus);
95+
return () => window.removeEventListener('focus', fetchStatus);
96+
}, []);
97+
98+
const authAdapter = useMemo(() => {
99+
return createAuthenticationAdapter({
100+
getNonce: async () => {
101+
const response = await fetch('/api/nonce');
102+
return await response.text();
103+
},
104+
105+
createMessage: ({ nonce, address, chainId }) => {
106+
return new SiweMessage({
107+
domain: window.location.host,
108+
address,
109+
statement: 'Sign in with Ethereum to the app.',
110+
uri: window.location.origin,
111+
version: '1',
112+
chainId,
113+
nonce,
114+
});
115+
},
116+
117+
getMessageBody: ({ message }) => {
118+
return message.prepareMessage();
119+
},
120+
121+
verify: async ({ message, signature }) => {
122+
verifyingRef.current = true;
123+
124+
try {
125+
const response = await fetch('/api/verify', {
126+
method: 'POST',
127+
headers: { 'Content-Type': 'application/json' },
128+
body: JSON.stringify({ message, signature }),
129+
});
130+
131+
const authenticated = Boolean(response.ok);
132+
133+
if (authenticated) {
134+
setAuthStatus(authenticated ? 'authenticated' : 'unauthenticated');
135+
}
136+
137+
return authenticated;
138+
} catch (error) {
139+
return false;
140+
} finally {
141+
verifyingRef.current = false;
142+
}
143+
},
144+
145+
signOut: async () => {
146+
setAuthStatus('unauthenticated');
147+
await fetch('/api/logout');
148+
},
149+
});
150+
}, []);
151+
152+
return (
153+
<WagmiConfig client={wagmiClient}>
154+
<RainbowKitAuthenticationProvider
155+
adapter={authAdapter}
156+
status={authStatus}
157+
>
158+
<RainbowKitProvider appInfo={demoAppInfo} chains={chains}>
159+
<Component {...pageProps} />
160+
</RainbowKitProvider>
161+
</RainbowKitAuthenticationProvider>
162+
</WagmiConfig>
163+
);
164+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { withIronSessionApiRoute } from 'iron-session/next';
2+
import { NextApiRequest, NextApiResponse } from 'next';
3+
import { ironOptions } from './../../lib/iron';
4+
5+
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
6+
const { method } = req;
7+
switch (method) {
8+
case 'GET':
9+
req.session.destroy();
10+
res.send({ ok: true });
11+
break;
12+
default:
13+
res.setHeader('Allow', ['GET']);
14+
res.status(405).end(`Method ${method} Not Allowed`);
15+
}
16+
};
17+
18+
export default withIronSessionApiRoute(handler, ironOptions);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { withIronSessionApiRoute } from 'iron-session/next';
2+
import { NextApiRequest, NextApiResponse } from 'next';
3+
import { ironOptions } from './../../lib/iron';
4+
5+
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
6+
const { method } = req;
7+
switch (method) {
8+
case 'GET':
9+
res.send({ address: req.session.siwe?.address });
10+
break;
11+
default:
12+
res.setHeader('Allow', ['GET']);
13+
res.status(405).end(`Method ${method} Not Allowed`);
14+
}
15+
};
16+
17+
export default withIronSessionApiRoute(handler, ironOptions);

0 commit comments

Comments
 (0)