├── .github └── workflows │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .releaserc ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── src ├── api.ts ├── express │ └── index.ts ├── index.ts ├── next │ └── index.ts ├── siweProvider.tsx ├── types.ts ├── useOptions.ts ├── useSession.ts ├── useSignIn.ts └── useSignOut.ts ├── tsconfig.json └── tsup.config.ts /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - next 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: "lts/*" 20 | - name: Install dependencies 21 | run: npm ci 22 | - name: Build 23 | run: npm run build 24 | - name: Release 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | run: npx semantic-release 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | { "name": "main" }, 4 | { "name": "next", "prerelease": true } 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 AJ May 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UseSIWE 2 | 3 | UseSIWE is a library that provides react hooks and API endpoints that make it 4 | dead simple to add Sign-In with Ethereum functionality to your react 5 | application. 6 | 7 | ### 🌈 Works with RainbowKit 8 | 9 | The easiest way to use this library is with RainbowKit! 10 | Check out the RainbowKit authentication adapter for UseSiwe here: 11 | https://github.com/random-bits-studio/rainbowkit-use-siwe-auth 12 | 13 | # Table of Contents 14 | 15 | - [Installation](#installation) 16 | - [Getting Started](#getting-started) 17 | - [Configure settings for iron-session](#configure-settings-for-iron-session) 18 | - [Setting up the API routes](#setting-up-the-api-routes) 19 | - [Next.js](#nextjs) 20 | - [Express.js](#expressjs) 21 | - [Wrapping your application with SiweProvider](#wrapping-your-application-with-siweprovider) 22 | - [Using the hooks](#using-the-hooks) 23 | - [Checking if a user is authenticated](#checking-if-a-user-is-authenticated) 24 | - [Signing In](#signing-in) 25 | - [Signing Out](#signing-out) 26 | - [API](#api) 27 | - [Types](#types) 28 | - [UseSiweOptions](#usesiweoptions) 29 | - [Components](#components) 30 | - [SiweProvider](#siweprovider) 31 | - [Hooks](#hooks) 32 | - [useSession](#usesession) 33 | - [useSignIn](#usesignin) 34 | - [useSignOut](#usesignout) 35 | - [useOptions](#useoptions) 36 | - [Routes](#routes) 37 | - [Next.js: SiweApi](#nextjs-siweapi) 38 | - [Express.js: SiweApi](#expressjs-siweapi) 39 | - [Functions](#functions) 40 | - [getSession](#getsession) 41 | - [createMessage](#createmessage) 42 | - [getMessageBody](#getmessagebody) 43 | - [verify](#verify) 44 | - [signOut](#signout) 45 | 46 | # Installation 47 | 48 | To install UseSIWE and it's dependencies run the following command: 49 | 50 | ``` 51 | npm install @randombits/use-siwe wagmi ethers iron-session 52 | ``` 53 | 54 | # Getting Started 55 | 56 | ## Configure settings for `iron-session` 57 | 58 | Copy and paste the following code into a new file in your project: 59 | 60 | ```ts 61 | // lib/ironOptions.ts 62 | 63 | import { IronSessionOptions } from 'iron-session'; 64 | 65 | if (!process.env.IRON_SESSION_PASSWORD) 66 | throw new Error('IRON_SESSION_PASSWORD must be set'); 67 | 68 | const ironOptions: IronSessionOptions = { 69 | password: process.env.IRON_SESSION_PASSWORD, 70 | cookieName: 'session', 71 | cookieOptions: { 72 | secure: process.env.NODE_ENV === "production", 73 | }, 74 | }; 75 | 76 | declare module "iron-session" { 77 | interface IronSessionData { 78 | address?: string | undefined; 79 | nonce?: string | undefined; 80 | } 81 | } 82 | 83 | export default ironOptions; 84 | ``` 85 | 86 | **Remember to set IRON_SESSION_PASSWORD** in your `.env.local` file for 87 | development, and in your production environment through your hosting 88 | provider settings. The password must be at least 32 characters long. You can 89 | use https://1password.com/password-generator/ to generate strong passwords. 90 | 91 | For full reference of possible options see: 92 | https://github.com/vvo/iron-session#ironoptions 93 | 94 | **Typing session data** 95 | The type definition of `IronSessionData` in the example above provides a type 96 | definition to the data passed to api functions in `req.session`. `address` and 97 | `nonce` are used and set by UseSIWE; if you plan on storing other data in the 98 | session, feel free to add additional types here. 99 | 100 | For more information see: 101 | https://github.com/vvo/iron-session#typing-session-data-with-typescript 102 | 103 | ## Setting up the API routes 104 | 105 | ### Next.js 106 | 107 | Copy and past the following code into `pages/api/auth/[[...route]].ts`: 108 | 109 | ```ts 110 | import { withIronSessionApiRoute } from "iron-session/next"; 111 | import ironOptions from "lib/ironOptions"; 112 | import { siweApi } from "@randombits/use-siwe/next" 113 | 114 | export default withIronSessionApiRoute(siweApi(), ironOptions); 115 | ``` 116 | 117 | ### Express.js 118 | 119 | To add auth routes to your existing express API, add the following: 120 | 121 | ```ts 122 | import express from "express"; 123 | import { ironSession } from "iron-session/express"; 124 | import ironOptions from "./ironOptions.js"; 125 | import { authRouter } from "@randombits/use-siwe/express"; 126 | 127 | const app = express(); 128 | 129 | // Add iron session middleware before all routes that will use session data 130 | app.use(ironSession(ironOptions)); 131 | 132 | // Your existing api routes here... 133 | 134 | // Add UseSIWE auth routes 135 | app.use('/auth', authRouter()); 136 | 137 | app.listen(3001); 138 | ``` 139 | 140 | ## Wrapping your application with `SiweProvider` 141 | 142 | Any component that uses the any of the UseSIWE hooks must be wrapped with the 143 | `SiweProvider` component. For a Next.js application we recommend doing so in 144 | `pages/_app.tsx` like in the example below: 145 | 146 | ```ts 147 | // pages/_app.tsx 148 | 149 | import type { AppProps } from 'next/app'; 150 | import { configureChains, mainnet } from 'wagmi'; 151 | import { publicProvider } from 'wagmi/providers/public'; 152 | import { SiweProvider } from '@randombits/use-siwe'; 153 | 154 | const { chains, provider, webSocketProvider } = configureChains( 155 | [mainnet], 156 | [publicProvider()], 157 | ); 158 | 159 | const client = createClient({ 160 | autoConnect: true, 161 | provider, 162 | webSocketProvider, 163 | }); 164 | 165 | export default function MyApp({ Component, pageProps }: AppProps) { 166 | return ( 167 | 168 | 169 | 170 | 171 | 172 | ); 173 | } 174 | ``` 175 | 176 | **Important:** The `SiweProvider` must be inside a `WagmiConfig` component. 177 | 178 | ## Using the hooks 179 | 180 | ### Checking if a user is authenticated 181 | 182 | #### Client-side 183 | 184 | Check to see is a user is authenticated with the `useSession` hook like in the 185 | example below: 186 | 187 | ```ts 188 | import { useSession } from "@randombits/use-siwe"; 189 | 190 | export const AuthCheck = () => { 191 | const { isLoading, authenticated, address } = useSession(); 192 | 193 | if (isLoading) return

Loading...

; 194 | if (!authenticated) return

Not authenticated

; 195 | return

{address} is Authenticated

; 196 | }; 197 | ``` 198 | 199 | #### Server-side 200 | 201 | For API routes, wrap your API handler with `withIronSessionApiRoute` and check 202 | to see if `req.session.address` is set. If a user is authenticated, 203 | `req.session.address` will be set to their address, otherwise it will be 204 | `undefined`. 205 | 206 | ```ts 207 | import ironOptions from '@/lib/ironOptions' 208 | import { withIronSessionApiRoute } from 'iron-session/next/dist' 209 | import type { NextApiHandler } from 'next' 210 | 211 | const handler: NextApiHandler = (req, res) => { 212 | if (!req.session.address) return res.status(401).send("Unauthorized"); 213 | res.status(200).send(`Hello, ${req.session.address}!`); 214 | } 215 | 216 | export default withIronSessionApiRoute(handler, ironOptions); 217 | ``` 218 | 219 | ### Signing In 220 | 221 | Login the user by calling the `signIn` function returned by the `useSignIn` 222 | hook: 223 | 224 | ```ts 225 | import { useSignIn } from "@randombits/use-siwe"; 226 | 227 | const SignInButton = () => { 228 | const { signIn, isLoading } = useSignIn(); 229 | return ; 230 | }; 231 | ``` 232 | 233 | ### Signing Out 234 | 235 | Logout the user by calling the `signOut` function returned by the `useSignOut` 236 | hook: 237 | 238 | ```ts 239 | import { useSignOut } from "@randombits/use-siwe"; 240 | 241 | const SignOutButton = () => { 242 | const { signOut, isLoading } = useSignOut(); 243 | return ; 244 | }; 245 | ``` 246 | 247 | # API 248 | 249 | ## Types 250 | 251 | ### UseSiweOptions 252 | 253 | UseSIWE accepts an object of options. Currently this consists of one optional 254 | setting: 255 | 256 | #### Usage 257 | 258 | ```ts 259 | const options: UseSiweOptions = { 260 | baseUrl: "/v2/api/auth", 261 | }; 262 | ``` 263 | 264 | #### Options 265 | 266 | - `baseUrl`, optional: The base url for the auth API endpoints that is 267 | prepended to all requests. Defaults to: `/api/auth` 268 | 269 | ## Components 270 | 271 | ### SiweProvider 272 | 273 | Context provider component that must wrap all components that use `useSession`, 274 | `useSignIn`, `useSignOut`, or `useOptions` hooks. 275 | 276 | #### Usage 277 | 278 | ```ts 279 | import type { AppProps } from 'next/app'; 280 | import { SiweProvider } from '@randombits/use-siwe'; 281 | 282 | export default function MyApp({ Component, pageProps }: AppProps) { 283 | return 284 | 285 | ; 286 | } 287 | ``` 288 | 289 | #### Props 290 | 291 | - `options`, Optional: A `UseSiweOptions` object. 292 | 293 | ## Hooks 294 | 295 | ### useSession 296 | 297 | A hook that returns the the current state of the users session. 298 | 299 | #### Usage 300 | 301 | ```ts 302 | import { useSession } from "@randombits/use-siwe"; 303 | 304 | export const Component = () => { 305 | const { isLoading, authenticated, address } = useSession(); 306 | 307 | if (isLoading) return
Loading...
; 308 | if (!authenticated) return
Not Signed In
; 309 | return
Hello, {address}!
; 310 | }; 311 | ``` 312 | 313 | #### Return Value 314 | 315 | Returns a `UseQueryResult` ([ref](https://tanstack.com/query/latest/docs/react/reference/useQuery)) 316 | augmented with the following: 317 | 318 | ```ts 319 | { 320 | authenticated: boolean; 321 | address?: string; 322 | nonce?: string; 323 | } & UseQueryResult 324 | ``` 325 | 326 | ### useSignIn 327 | 328 | A hook that returns a `signIn` function that will initiate a SIWE flow, as well 329 | as the status of that signIn process. 330 | 331 | #### Usage 332 | 333 | ```ts 334 | import { useSignIn } from "@randombits/use-siwe"; 335 | 336 | const SignInButton = () => { 337 | const { signIn, isLoading } = useSignIn(); 338 | return ; 339 | }; 340 | ``` 341 | 342 | #### Options 343 | 344 | ```ts 345 | { 346 | onSuccess: () => void, 347 | onError: () => void, 348 | } 349 | ``` 350 | 351 | #### Return Value 352 | 353 | Returns a `UseMutationResult` ([ref](https://tanstack.com/query/latest/docs/react/reference/useMutation)) 354 | augmented with the following: 355 | 356 | ```ts 357 | { 358 | signIn: () => void, 359 | SignInAsync: () => Promise, 360 | } & UseMutationResult 361 | ``` 362 | 363 | ### useSignOut 364 | 365 | A hook that returns a `signOut` function that when called will sign out the 366 | current user and disconnect their wallet. 367 | 368 | #### Usage 369 | 370 | ```ts 371 | import { useSignOut } from "@randombits/use-siwe"; 372 | 373 | const SignOutButton = () => { 374 | const { signOut, isLoading } = useSignOut(); 375 | return ; 376 | }; 377 | ``` 378 | 379 | #### Options 380 | 381 | ```ts 382 | { 383 | onSuccess: () => void, 384 | onError: () => void, 385 | } 386 | ``` 387 | 388 | #### Return Value 389 | 390 | Returns a `UseMutationResult` ([ref](https://tanstack.com/query/latest/docs/react/reference/useMutation)) 391 | augmented with the following: 392 | 393 | ```ts 394 | { 395 | signOut: () => void, 396 | SignOutAsync: () => Promise, 397 | } & UseMutationResult 398 | ``` 399 | 400 | ### useOptions 401 | 402 | A hook that simply returns the options that have been set by in the 403 | `SiweProvider` component. 404 | 405 | #### Usage 406 | 407 | ```ts 408 | import { useOptions, verify } from "@randombits/use-siwe"; 409 | 410 | const verifyButton = (props) => { 411 | const options = useOptions(); 412 | const handleClick = () => verify({ 413 | message: props.message, 414 | signature: props.signature, 415 | }, options); 416 | 417 | return ; 418 | }; 419 | ``` 420 | 421 | #### Return Value 422 | 423 | ```ts 424 | useSiweOptions 425 | ``` 426 | 427 | ## Routes 428 | 429 | ### Next.js: SiweApi 430 | 431 | A function that returns a `NextApiHandler` that will handle all auth API 432 | routes. 433 | 434 | #### Usage 435 | 436 | ```ts 437 | import { withIronSessionApiRoute } from "iron-session/next"; 438 | import ironOptions from "lib/ironOptions"; 439 | import { siweApi } from "@randombits/use-siwe/next" 440 | 441 | export default withIronSessionApiRoute(siweApi(), ironOptions); 442 | ``` 443 | 444 | #### Return Value 445 | 446 | ```ts 447 | NextApiHandler 448 | ``` 449 | 450 | ### Express.js: SiweApi 451 | 452 | A function that returns an express `Router` that will handle all auth API 453 | routes. 454 | 455 | #### Usage 456 | 457 | ```ts 458 | import express from "express"; 459 | import { ironSession } from "iron-session/express"; 460 | import ironOptions from "./ironOptions.js"; 461 | import { authRouter } from "@randombits/use-siwe/express"; 462 | 463 | const app = express(); 464 | 465 | app.use(ironSession(ironOptions)); 466 | app.use('/auth', authRouter()); 467 | 468 | app.listen(3001); 469 | ``` 470 | 471 | #### Return Value 472 | 473 | ```ts 474 | Router 475 | ``` 476 | 477 | ## Functions 478 | 479 | ### getSession 480 | 481 | A function to retrieve the session data where using a hook doesn't make sense. 482 | 483 | #### Usage 484 | 485 | ```ts 486 | import { getSession } from "@randombits/use-siwe"; 487 | 488 | const addressOrNull = async () => { 489 | const { address } = await getSession(); 490 | if (!address) return null; 491 | return address; 492 | }; 493 | ``` 494 | 495 | #### Args 496 | 497 | - `options?: UseSiweOptions` 498 | 499 | #### Return Value 500 | 501 | ```ts 502 | { 503 | authenticated: boolean; 504 | address?: string; 505 | nonce?: string; 506 | } 507 | ``` 508 | 509 | ### createMessage 510 | 511 | Returns a `SiweMessage` for the given address, chainId, and nonce. 512 | 513 | #### Usage 514 | 515 | ```ts 516 | import { createMessage, getMessageBody } from "@randombits/use-siwe"; 517 | 518 | const debugMessage = (address, chainId, nonce) => { 519 | const message = createMessage({ address, chainId, nonce }); 520 | const messageBody = getMessageBody({ message }); 521 | console.log({ message, messageBody }); 522 | }; 523 | ``` 524 | 525 | #### Args 526 | 527 | - `args: MessageArgs` 528 | 529 | ```ts 530 | type MessageArgs = { 531 | address: string, 532 | chainId: number, 533 | nonce: string, 534 | }; 535 | ``` 536 | 537 | #### Return Value 538 | 539 | ```ts 540 | SiweMessage 541 | ``` 542 | 543 | ### getMessageBody 544 | 545 | Returns a message ready to be signed according with the type defined in the 546 | SiweMessage object. 547 | 548 | #### Usage 549 | 550 | ```ts 551 | import { createMessage, getMessageBody } from "@randombits/use-siwe"; 552 | 553 | const debugMessage = (address, chainId, nonce) => { 554 | const message = createMessage({ address, chainId, nonce }); 555 | const messageBody = getMessageBody({ message }); 556 | console.log({ message, messageBody }); 557 | }; 558 | ``` 559 | 560 | #### Args 561 | 562 | - `args: { message: SiweMessage }` 563 | 564 | #### Return Value 565 | 566 | ```ts 567 | string 568 | ``` 569 | 570 | ### verify 571 | 572 | Takes a message and a signature as arguments and attempts to verify them using 573 | the auth API. A successful verification will create a session for the user. 574 | 575 | #### Usage 576 | 577 | ```ts 578 | import { verify } from "@randombits/use-siwe"; 579 | 580 | const verifyButton = (props) => { 581 | const handleClick = () => { 582 | const success = verify({ 583 | message: props.message, 584 | signature: props.signature, 585 | }); 586 | 587 | if (!success) return console.error("VERIFICATION FAILED"); 588 | console.log("SIGNATURE VERIFIED"); 589 | }; 590 | 591 | return ; 592 | }; 593 | ``` 594 | 595 | #### Args 596 | 597 | - `args: VerifyArgs` 598 | - `options?: UseSiweOptions` 599 | 600 | ```ts 601 | type VerifyArgs = { 602 | message: SiweMessage, 603 | signature: string, 604 | }; 605 | ``` 606 | 607 | #### Return Value 608 | 609 | ```ts 610 | boolean 611 | ``` 612 | 613 | ### signOut 614 | 615 | A function to sign out the user where using a hook doesn't make sense. 616 | 617 | #### Usage 618 | 619 | ```ts 620 | import { signOut } from "@randombits/use-siwe"; 621 | 622 | // Logout a user after 1 hour 623 | setTimeout(async () => { 624 | await signOut(); 625 | window.location.href = "/session-expired"; 626 | }, 60 * 60 * 1000); 627 | ``` 628 | 629 | #### Args 630 | 631 | - `options?: UseSiweOptions` 632 | 633 | #### Return Value 634 | 635 | ```ts 636 | Promise 637 | ``` 638 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@randombits/use-siwe", 3 | "version": "0.0.0-semantically-released", 4 | "description": "React hook and API endpoints that provide Sign In With Ethereum support", 5 | "module": "./dist/index.js", 6 | "main": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "type": "module", 9 | "files": [ 10 | "dist/**", 11 | "README.md", 12 | "LICENSE.md" 13 | ], 14 | "exports": { 15 | ".": { 16 | "import": "./dist/index.js", 17 | "require": "./dist/index.cjs", 18 | "types": "./dist/index.d.ts" 19 | }, 20 | "./*": { 21 | "import": "./dist/*/index.js", 22 | "require": "./dist/*/index.cjs", 23 | "types": "./dist/*/index.d.ts" 24 | } 25 | }, 26 | "typesVersions": { 27 | "*": { 28 | "next": [ 29 | "./dist/next/index.d.ts" 30 | ], 31 | "express": [ 32 | "./dist/express/index.d.ts" 33 | ] 34 | } 35 | }, 36 | "scripts": { 37 | "build": "tsup", 38 | "prepare": "husky install", 39 | "lint": "prettier --check ." 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/random-bits-studio/use-siwe.git" 44 | }, 45 | "keywords": [ 46 | "SIWE", 47 | "Next.js", 48 | "auth", 49 | "authentication", 50 | "Iron Session", 51 | "Session", 52 | "Ethereum", 53 | "Web3" 54 | ], 55 | "author": "AJ May ", 56 | "license": "MIT", 57 | "bugs": { 58 | "url": "https://github.com/random-bits-studio/use-siwe/issues" 59 | }, 60 | "homepage": "https://github.com/random-bits-studio/use-siwe#readme", 61 | "devDependencies": { 62 | "@types/express": "^4.17.15", 63 | "@types/react": "^18.0.20", 64 | "husky": "^8.0.3", 65 | "iron-session": "^6.3.1", 66 | "lint-staged": "^13.1.0", 67 | "next": "^13.1.2", 68 | "prettier": "^2.8.3", 69 | "tsup": "^6.5.0", 70 | "typescript": "^4.9.4", 71 | "wagmi": "^0.10.10" 72 | }, 73 | "dependencies": { 74 | "@tanstack/react-query": "^4.22.0", 75 | "siwe": "^2.0.5", 76 | "zod": "^3.20.2", 77 | "zod-validation-error": "^0.3.0" 78 | }, 79 | "peerDependencies": { 80 | "express": ">=4", 81 | "iron-session": ">=6", 82 | "next": ">=10", 83 | "react": ">=17", 84 | "wagmi": ">=0.9.0" 85 | }, 86 | "peerDependenciesMeta": { 87 | "wagmi": { 88 | "optional": true 89 | }, 90 | "iron-session": { 91 | "optional": true 92 | }, 93 | "next": { 94 | "optional": true 95 | }, 96 | "express": { 97 | "optional": true 98 | }, 99 | "react": { 100 | "optional": true 101 | } 102 | }, 103 | "publishConfig": { 104 | "access": "public" 105 | }, 106 | "lint-staged": { 107 | "*.{js,json,css,md}": "prettier --write" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from 'http'; 2 | import { generateNonce, SiweMessage } from 'siwe'; 3 | import { fromZodError } from 'zod-validation-error'; 4 | import { GetSessionResponse, signInRequestSchema, SignInResponse, SignOutResponse } from './types.js'; 5 | 6 | interface Request extends IncomingMessage { 7 | body: any; 8 | } 9 | 10 | interface Response extends ServerResponse { 11 | json: (body: T) => void; 12 | send: (body: T) => void; 13 | status: (statusCode: number) => Response; 14 | } 15 | 16 | type RequestHandler = (req: Request, res: Response) => void; 17 | 18 | export const getSession: RequestHandler = async (req, res) => { 19 | if (req.session.address) { 20 | return res.json({ 21 | authenticated: true, 22 | address: req.session.address, 23 | }); 24 | } 25 | 26 | if (!req.session.nonce) { 27 | req.session.nonce = generateNonce(); 28 | await req.session.save(); 29 | } 30 | 31 | return res.json({ 32 | authenticated: false, 33 | nonce: req.session.nonce, 34 | }); 35 | }; 36 | 37 | export const signIn: RequestHandler = async (req, res) => { 38 | const { nonce } = req.session; 39 | if (!nonce) return res.status(400).send("Bad Request"); 40 | 41 | const parsedBody = signInRequestSchema.safeParse(req.body); 42 | if (!parsedBody.success) { 43 | const error = fromZodError(parsedBody.error); 44 | return res.status(400).send(error.message); 45 | } 46 | const { message, signature } = parsedBody.data; 47 | 48 | const { success, error, data } = await new SiweMessage(message).verify({ 49 | signature, 50 | nonce, 51 | // domain, // TODO: verify domain is correct too 52 | }); 53 | 54 | if (!success && error) return res.status(400).send(error.type); 55 | if (!success) return res.status(500).send("Unknown Error"); 56 | 57 | req.session.nonce = undefined; 58 | req.session.address = data.address; 59 | await req.session.save(); 60 | 61 | return res.send("OK"); 62 | }; 63 | 64 | export const signOut: RequestHandler = async (req, res) => { 65 | if (!req.session.address) return res.status(400).send("Bad Request"); 66 | 67 | req.session.nonce = generateNonce(); 68 | req.session.address = undefined; 69 | await req.session.save(); 70 | 71 | return res.send("OK"); 72 | }; 73 | 74 | export const methodNotAllowed: RequestHandler = (_req, res) => 75 | res.status(403).send("Method Not Allowed"); 76 | 77 | export const notFound: RequestHandler = (_req, res) => 78 | res.status(404).send("Not Found"); 79 | -------------------------------------------------------------------------------- /src/express/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { getSession, methodNotAllowed, notFound, signIn, signOut } from "../api.js"; 3 | 4 | export const authRouter = () => { 5 | const router = express.Router(); 6 | 7 | router.route('/') 8 | .get(getSession) 9 | .all(methodNotAllowed); 10 | 11 | router.route('/signin') 12 | .post(signIn) 13 | .all(methodNotAllowed); 14 | 15 | router.route('/signout') 16 | .post(signOut) 17 | .all(methodNotAllowed); 18 | 19 | router.route('*') 20 | .all(notFound); 21 | 22 | return router; 23 | }; 24 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { SiweProvider } from "./siweProvider.js"; 2 | export { getSession, useSession } from "./useSession.js" 3 | export { createMessage, getMessageBody, verify, useSignIn } from "./useSignIn.js" 4 | export { signOut, useSignOut } from "./useSignOut.js" 5 | export { useOptions } from "./useOptions.js" 6 | export type { UseSiweOptions } from "./types.js" 7 | -------------------------------------------------------------------------------- /src/next/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import { getSession, methodNotAllowed, notFound, signIn, signOut } from "../api.js"; 3 | 4 | export const siweApi = () => async (req: NextApiRequest, res: NextApiResponse) => { 5 | let { route } = req.query; 6 | if (route instanceof Array) route = route[0]; 7 | const { method } = req; 8 | 9 | switch (route) { 10 | case undefined: 11 | switch (method) { 12 | case "GET": 13 | return getSession(req, res); 14 | default: 15 | return methodNotAllowed(req, res); 16 | } 17 | 18 | case "signin": 19 | switch (method) { 20 | case "POST": 21 | return signIn(req, res); 22 | default: 23 | return methodNotAllowed(req, res); 24 | } 25 | 26 | case "signout": 27 | switch (method) { 28 | case "POST": 29 | return signOut(req, res); 30 | default: 31 | return methodNotAllowed(req, res); 32 | } 33 | 34 | default: 35 | return notFound(req, res); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/siweProvider.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | import React, { createContext, PropsWithChildren } from "react"; 3 | import { UseSiweOptions } from "./types.js"; 4 | 5 | export const queryContext = createContext(undefined); 6 | export const optionsContext = createContext({}); 7 | const queryClient = new QueryClient({ 8 | defaultOptions: { 9 | queries: { 10 | staleTime: 30 * 1000, 11 | }, 12 | }, 13 | }); 14 | 15 | type SiweProviderProps = PropsWithChildren & { 16 | options?: UseSiweOptions, 17 | }; 18 | 19 | export const SiweProvider = ({ children, options = {} }: SiweProviderProps) => ( 20 | 21 | 22 | {children} 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import "iron-session"; 2 | import { z } from "zod"; 3 | 4 | declare module "iron-session" { 5 | interface IronSessionData { 6 | address?: string | undefined; 7 | nonce?: string | undefined; 8 | } 9 | } 10 | 11 | const siweMessageSchema = z.object({ 12 | domain: z.string(), 13 | address: z.string(), 14 | statement: z.string().optional(), 15 | uri: z.string(), 16 | version: z.string(), 17 | chainId: z.number(), 18 | nonce: z.string(), 19 | issuedAt: z.string().optional(), 20 | expirationTime: z.string().optional(), 21 | notBefore: z.string().optional(), 22 | requestId: z.string().optional(), 23 | resources: z.array(z.string()).optional(), 24 | }); 25 | 26 | export const signInRequestSchema = z.object({ 27 | message: siweMessageSchema, 28 | signature: z.string(), 29 | }); 30 | 31 | export type GetSessionResponse = { 32 | authenticated: boolean, 33 | address?: string, 34 | nonce?: string 35 | }; 36 | 37 | export type SignInRequest = z.infer; 38 | 39 | export type SignInResponse = string; 40 | 41 | export type SignOutResponse = string; 42 | 43 | export type UseSiweOptions = { 44 | baseUrl?: string, 45 | }; 46 | -------------------------------------------------------------------------------- /src/useOptions.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { optionsContext } from "./siweProvider.js"; 3 | import { UseSiweOptions } from "./types.js" 4 | 5 | const defaultOptions: Required = { 6 | baseUrl: "/api/auth", 7 | } 8 | 9 | export const parseOptions = (options: UseSiweOptions = {}) => { 10 | return { ...defaultOptions, ...options }; 11 | } 12 | 13 | export const useOptions = () => { 14 | const options = useContext(optionsContext); 15 | return parseOptions(options); 16 | } 17 | -------------------------------------------------------------------------------- /src/useSession.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import { useContext } from "react"; 3 | import { parseOptions } from "./useOptions.js"; 4 | import { queryContext, optionsContext } from "./siweProvider.js"; 5 | import { GetSessionResponse, UseSiweOptions } from "./types.js"; 6 | 7 | export const getSession = async (options?: UseSiweOptions) => { 8 | const { baseUrl } = parseOptions(options); 9 | const res = await fetch(baseUrl); 10 | if (!res.ok) throw new Error(res.statusText); 11 | return res.json() as Promise; 12 | } 13 | 14 | export const useSession = () => { 15 | const options = useContext(optionsContext); 16 | const { data, ...rest } = useQuery({ 17 | queryKey: ["session"], 18 | queryFn: () => getSession(options), 19 | context: queryContext, 20 | }); 21 | 22 | return { ...rest, ...data }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/useSignIn.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient, UseQueryOptions } from "@tanstack/react-query"; 2 | import { useContext } from "react"; 3 | import { SiweMessage } from "siwe"; 4 | import { useAccount, useNetwork, useSignMessage } from "wagmi"; 5 | import { parseOptions } from "./useOptions.js"; 6 | import { optionsContext, queryContext } from "./siweProvider.js"; 7 | import { UseSiweOptions } from "./types.js"; 8 | import { useSession } from "./useSession.js"; 9 | 10 | type UseSignInOptions = Pick, "onSuccess" | "onError"> 11 | type MessageArgs = { 12 | address: string, 13 | chainId: number, 14 | nonce: string, 15 | }; 16 | type VerifyArgs = { 17 | message: SiweMessage, 18 | signature: string, 19 | }; 20 | 21 | export const createMessage = (args: MessageArgs) => 22 | new SiweMessage({ 23 | ...args, 24 | domain: window.location.host, 25 | uri: window.location.origin, 26 | version: "1", 27 | }); 28 | 29 | export const getMessageBody = ({ message }: { message: SiweMessage }) => 30 | message.prepareMessage(); 31 | 32 | export const verify = async (args: VerifyArgs, options?: UseSiweOptions) => { 33 | const { baseUrl } = parseOptions(options); 34 | const res = await fetch(`${baseUrl}/signin`, { 35 | headers: { 'Content-Type': 'application/json' }, 36 | method: "POST", 37 | body: JSON.stringify(args), 38 | }); 39 | 40 | return res.ok; 41 | } 42 | 43 | export const useSignIn = ({ onSuccess, onError }: UseSignInOptions = {}) => { 44 | const { address } = useAccount(); 45 | const { chain } = useNetwork(); 46 | const { nonce } = useSession(); 47 | const { signMessageAsync } = useSignMessage(); 48 | const queryClient = useQueryClient({ 49 | context: queryContext, 50 | }); 51 | const options = useContext(optionsContext); 52 | 53 | const { mutate, mutateAsync, ...rest } = useMutation( 54 | async () => { 55 | const message = createMessage({ address, chainId: chain.id, nonce }); 56 | const messageBody = getMessageBody({ message: message }); 57 | const signature = await signMessageAsync({ message: messageBody }); 58 | const result = await verify({ message, signature }, options); 59 | if (!result) throw new Error("Verification Failed"); 60 | }, 61 | { 62 | onSuccess: () => { 63 | queryClient.invalidateQueries({ queryKey: ["session"] }); 64 | if (onSuccess) onSuccess(); 65 | }, 66 | onError, 67 | context: queryContext, 68 | }, 69 | ); 70 | 71 | return { ...rest, signIn: mutate, SignInAsync: mutateAsync }; 72 | }; 73 | -------------------------------------------------------------------------------- /src/useSignOut.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient, UseQueryOptions } from "@tanstack/react-query"; 2 | import { useContext } from "react"; 3 | import { useAccount, useDisconnect } from "wagmi"; 4 | import { parseOptions } from "./useOptions.js"; 5 | import { optionsContext, queryContext } from "./siweProvider.js"; 6 | import { UseSiweOptions } from "./types.js"; 7 | import { useSession } from "./useSession.js"; 8 | 9 | type UseSignOutOptions = Pick, "onSuccess" | "onError"> 10 | 11 | export const signOut = async (options?: UseSiweOptions) => { 12 | const { baseUrl } = parseOptions(options); 13 | const res = await fetch(`${baseUrl}/signout`, { method: "POST" }); 14 | if (!res.ok) throw new Error(res.statusText); 15 | }; 16 | 17 | export const useSignOut = ({ onSuccess, onError }: UseSignOutOptions = {}) => { 18 | const { authenticated } = useSession(); 19 | const { isConnected } = useAccount(); 20 | const { disconnectAsync } = useDisconnect(); 21 | const queryClient = useQueryClient({ 22 | context: queryContext, 23 | }); 24 | const options = useContext(optionsContext); 25 | 26 | const { mutate, mutateAsync, ...rest } = useMutation( 27 | async () => { 28 | if (authenticated) await signOut(options); 29 | if (isConnected) await disconnectAsync(); 30 | }, 31 | { 32 | onSuccess: () => { 33 | queryClient.invalidateQueries({ queryKey: ["session"] }); 34 | if (onSuccess) onSuccess(); 35 | }, 36 | onError, 37 | context: queryContext, 38 | }, 39 | ); 40 | 41 | return { ...rest, signOut: mutate, SignOutAsync: mutateAsync }; 42 | }; 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "moduleResolution": "node16", 5 | "esModuleInterop": true, 6 | "target": "ES2020", 7 | "lib": ["esnext", "dom"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts", "src/next/index.ts", "src/express/index.ts"], 5 | splitting: false, 6 | sourcemap: true, 7 | dts: true, 8 | minify: true, 9 | format: ["esm", "cjs"], 10 | }); 11 | --------------------------------------------------------------------------------