8000 feat: SIWE plugin by rokitgg · Pull Request #2579 · better-auth/better-auth · GitHub
[go: up one dir, main page]

Skip to content

feat: SIWE plugin #2579

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft

feat: SIWE plugin #2579

wants to merge 9 commits into from

Conversation

rokitgg
Copy link
@rokitgg rokitgg commented May 7, 2025

Aims to close #41

Tried my best to implement SIWE within the boundaries of better-auth, and seems like this draft may be a good starting point for it! Although I do feel like a bit of discussion is needed on some topics.

Things to consider:

  1. As of this draft, the plugin requires the official siwe npm package as a dependency, which may not be ideal
  2. Comprehensive testing could include mocking a wallet connection to generate a real signature - as of now tests are bit too shy in my opinion, should be improved.
  3. Happy to help with documentation on how to actually implement a web3 login feature using this plugin, which absolutely needs some proper docs to help beginners.

Any feedback/contributions are highly welcomed!

Copy link
vercel bot commented May 7, 2025

@rokitgg is attempting to deploy a commit to the better-auth Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
pkg-pr-new bot commented May 7, 2025

Open in StackBlitz

npm i https://pkg.pr.new/better-auth/better-auth@2579
npm i https://pkg.pr.new/better-auth/better-auth/@better-auth/cli@2579
npm i https://pkg.pr.new/better-auth/better-auth/@better-auth/expo@2579
npm i https://pkg.pr.new/better-auth/better-auth/@better-auth/stripe@2579

commit: 36f5ba6

@reslear
Copy link
Contributor
reslear commented May 7, 2025

we need remove siwe packages and use user functions for siwe createmessage or generateNonce
for example for use
https://viem.sh/docs/siwe/utilities/createSiweMessage

or https://docs.reown.com/appkit/vue/core/siwe

@8times4
Copy link
8times4 commented May 7, 2025

Given that viem directly implements siwe, using siwe's bloat is not so great imho.

it's be also beneficial to make the chainId or chain itself a plugin option, given that the message can contain the chainId of any other chain, however there'd be no direct way to modify that from where the message is crafted.
(Meaning you could not verify a message using the snippet below for any other chainId.)

Ps; 've also made the rpc url an option in mine, in case I need to use a custom rpc when validating via publicClient.

Here's a small snippet of my local plugin I've been using:

import { generateSiweNonce, parseSiweMessage } from "viem/siwe";
import { createPublicClient, http, type Address, type Hex } from "viem";

const publicClient = createPublicClient({
  chain: mainnet,
  transport: http()
})

...

const nonce = generateSiweNonce();

...

const parsedMessage = parseSiweMessage(message);

...


            const isValid = await publicClient.verifyMessage({
              message,
              signature: signature as Hex,
              address: parsedMessage.address,
            });

So pretty much almost the same what comes with siwe.
docs for viem: https://viem.sh/docs/siwe/actions/verifySiweMessage

@rokitgg
Copy link
Author
rokitgg commented May 7, 2025

we need remove siwe packages and use user functions for siwe createmessage or generateNonce for example for use https://viem.sh/docs/siwe/utilities/createSiweMessage

or https://docs.reown.com/appkit/vue/core/siwe

Agree but packages like viem use siwe under the hood so isn't that quite the same problem after all?

@reslear
Copy link
Contributor
reslear commented May 7, 2025

we need remove siwe packages and use user functions for siwe createmessage or generateNonce for example for use viem.sh/docs/siwe/utilities/createSiweMessage
or docs.reown.com/appkit/vue/core/siwe

Agree but packages like viem use siwe under the hood so isn't that quite the same problem after all?

its no problem, i mean let the user do it :

import { betterAuth } from "better-auth"

// import viem or siwe or other libs ...
 
export const auth = betterAuth({
    // ... other config options
    plugins: [
        siwe({ 
                async generateSiweNonce() { 
					// Implement 
				}, 
				
				async parseSiweMessage() {
				
				}
        }) 
    ]
})

@rokitgg
Copy link
Author
rokitgg commented May 7, 2025

we need remove siwe packages and use user functions for siwe createmessage or generateNonce for example for use viem.sh/docs/siwe/utilities/createSiweMessage
or docs.reown.com/appkit/vue/core/siwe

Agree but packages like viem use siwe under the hood so isn't that quite the same problem after all?

its no problem, i mean let the user do it :

import { betterAuth } from "better-auth"

// import viem or siwe or other libs ...
 
export const auth = betterAuth({
    // ... other config options
    plugins: [
        siwe({ 
                async generateSiweNonce() { 
					// Implement 
				}, 
				
				async parseSiweMessage() {
				
				}
        }) 
    ]
})

Ohh okay now I get what u mean by user functions, that def sounds like the best solution!

@rokitgg
Copy link
Author
rokitgg commented May 7, 2025

So plugin options could look something the likes of

interface SiweOptions {
	domain: string;
	generateSiweNonce: () => Promise<string>;
	parseSiweMessage: (message: string, signature: string) => Promise<boolean>;
}

Since the siwe message should be generated on the client, that's probably all it takes in terms of plugin user functions - lmk if missed something

@reslear
Copy link
Contributor
reslear commented May 7, 2025

@rokitgg and need verifyMessage

@b-bot
Copy link
b-bot commented May 8, 2025

@rokitgg I was about to start writing this as a community plugin 'cause I need it to migrate a project from Auth.js 😅
Happy to assist you here though.

On your question with external deps, I think adding siwe is the preferred option over writing your own functions that will be complex to maintain as new standards emerge and since any auth with eth will always need this package it's kinda an essential part of the flow.

We should however add it to peer deps so the user is expected to install it to keep bundle size down and add a note in the docs.

Not sure what @8times4 means by bloat as it's 76.4 kB unpacked compared to viem 16.4 MB, so don't use that.

If going with @reslear solution (user defines the functions) then add a custom RPC to the config as the http() transport is rate limited and therefore not production ready.

import { http, createPublicClient } from "viem";
import { mainnet, sepolia, type Chain } from "viem/chains";

export function getChainPublicClient(chain: Chain) {
  const apiKey = process.env.PROVIDER_API_KEY;
  const rpcUrl =
    chain === mainnet
      ? "https://api.etherscan.io/api"
      : "https://sepolia.etherscan.io/api";
  const useCustomRpc = (chain === mainnet || chain === sepolia) && !!apiKey;
  return createPublicClient({
    chain: chain,
    transport: useCustomRpc ? http(`${rpcUrl}/${apiKey}`) : http(),
  });
}

@8times4
Copy link
8times4 commented May 8, 2025

@rokitgg I was about to start writing this as a community plugin 'cause I need it to migrate a project from Auth.js 😅 Happy to assist you here though.

On your question with external deps, I think adding siwe is the preferred option over writing your own functions that will be complex to maintain as new standards emerge and since any auth with eth will always need this package it's kinda an essential part of the flow.

We should however add it to peer deps so the user is expected to install it to keep bundle size down and add a note in the docs.

Not sure what @8times4 means by bloat as it's 76.4 kB unpacked compared to viem 16.4 MB, so defintely don't use that.

ah, my bad then, I stand corrected on bloat there.
My thought process went along as viem being used anyways in a web3 project(given that most wallet connector adapters like rainbow/appkit/family rely on viem anyways), so having it built in would not really add size.
On top of that, I was under the assumption that the siwe package is not maintained as well as viem, given their ecosystem, but I was wrong there as well. :)

unrelated; I just noticed that siwe is also implemented in ox, which is wevm's Ethereum StdLib - wevm is the parent company of viem -
https://oxlib.sh/api/Siwe

});

if (!user) {
const tempEmail = `${ctx.body.publicKey}@${process.env.NEXT_PUBLIC_BASE_URL}`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use

const { emailDomainName = getOrigin(ctx.context.baseURL) } = options || {};`
const email = `${publicKey}@${emailDomainName}`;

if (!user) {
const tempEmail = `${ctx.body.publicKey}@${process.env.NEXT_PUBLIC_BASE_URL}`;
user = await ctx.context.internalAdapter.createUser({
name: ctx.body.publicKey,
Copy link
@b-bot b-bot May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed opportunity to retrieve ENS name, take it in as an optional param from the client and fallback to something else other than publicKey. Check the Anonymous plugin AnonymousOptions interface for some other ideas.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I had ENSName and ENSAvatar in a previous implementation but dropped it before drafting PR because it depended on wagmi. But happy to add that!

import type { User } from "../../types";

export interface SiweUser extends User {
publicKey?: string;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

walletAddress would be better as publicKey is too general IMO.

@b-bot
Copy link
b-bot commented May 8, 2025

@8times4 Yeah viem is industry standard at this point so most will be using that for sure I get your thinking. But better to decouple in case users want to use ethers etc.

You're actually not wrong on SIWE maintenance it has seen better days 😅 Maybe we use it for now and write our own utils phasing it out 1:1 at some point. This industry moves really fast though so we will be stuck on maintaining it ofc. Tradeoffs.

@rokitgg
Copy link
Author
rokitgg commented May 8, 2025

@rokitgg I was about to start writing this as a community plugin 'cause I need it to migrate a project from Auth.js 😅 Happy to assist you here though.

On your question with external deps, I think adding siwe is the preferred option over writing your own functions that will be complex to maintain as new standards emerge and since any auth with eth will always need this package it's kinda an essential part of the flow.

We should however add it to peer deps so the user is expected to install it to keep bundle size down and add a note in the docs.

Not sure what @8times4 means by bloat as it's 76.4 kB unpacked compared to viem 16.4 MB, so don't use that.

If going with @reslear solution (user defines the functions) then add a custom RPC to the config as the http() transport is rate limited and therefore not production ready.

import { http, createPublicClient } from "viem";
import { mainnet, sepolia, type Chain } from "viem/chains";

export function getChainPublicClient(chain: Chain) {
  const apiKey = process.env.PROVIDER_API_KEY;
  const rpcUrl =
    chain === mainnet
      ? "https://api.etherscan.io/api"
      : "https://sepolia.etherscan.io/api";
  const useCustomRpc = (chain === mainnet || chain === sepolia) && !!apiKey;
  return createPublicClient({
    chain: chain,
    transport: useCustomRpc ? http(`${rpcUrl}/${apiKey}`) : http(),
  });
}

Thank you so much for this comments! I really do agree with your vision.

 - Add ensName as an optional param from the client
 - Add emailDomainName to plugin options - fallback to getOrigin()
- Move schema to a separate file
Copy link
jazzberry-ai bot commented May 8, 2025

Bug Report

The SIWE plugin's verify endpoint constructs the user's email address using the emailDomainName option, defaulting to the origin of the baseURL. This can lead to unexpected behavior when the desired email domain differs from the baseURL's origin.

Name Severity Example test case Description
Incorrect email domain Medium Create a test instance with emailDomainName = "customdomain.com" and call the /siwe/verify endpoint. Assert that the created user's email is <walletAddress>@customdomain.com. The plugin should use the specified emailDomainName to construct the email address, regardless of the baseURL.
Missing test case Low Add a new test case to assert that the created user's email is <walletAddress>@customdomain.com when the the options contains emailDomainName and verify endpoint is called. The plugin should have a test cases to verify the function is working as intended.

Comments? Email us.

Copy link
jazzberry-ai bot commented May 8, 2025

Bug Report

Name Severity Example test case Description
Missing required: true on walletAddress Low Create a user through a different plugin (e.g. username/password) without providing a walletAddress. Try to sign in with SIWE for that user. The walletAddress field in schema.ts is set to required: false. This allows users to be created without a walletAddress, leading to potential issues with the SIWE plugin's functionality and potentially circumventing SIWE authentication.
Generic Error Message Medium Attempt to sign in with an invalid SIWE message. The error message "Something went wrong. Please try again later." in index.ts is too generic and doesn't provide enough information for debugging. Specific errors should be provided.
Missing ENS Name Validation Low Sign in with SIWE using a malicious ENS name, such as one containing Javascript. The code does not validate the ENS name, which could be a potential XSS vector.
Missing Fallback ENS Name Low Sign in with SIWE and the user does not have an ENS Name The code has a TODO to fallback to something else other than walletAddress if there is no ENS name, causing a bad user experience.

Comments? Email us.

@rokitgg
Copy link
Author
rokitgg commented May 8, 2025

Damn this jazzberry bot really is smart 😂

Copy link
jazzberry-ai bot commented May 8, 2025

Bug Report

Name Severity Example test case Description
Missing avatar field during user creation in SIWE plugin Medium When a new user signs in with SIWE (Sign-In With Ethereum), the avatar field is not included in the createUser call in packages/better-auth/src/plugins/siwe/index.ts. This might cause issues with certain adapter implementations that require the avatar field. The createUser method in the internal adapter might expect an avatar field. Although the avatar field is specified as optional in some areas, different database adapter implementations may strictly require the avatar field and throw an error when it is missing during user creation, leading to an internal server error during SIWE authentication. A default value of empty string has been added to mitigate this potential issue.

Comments? Email us.

Copy link
jazzberry-ai bot commented May 8, 2025

Bug Report

Name Severity Example test case Description
Insecure Email Address Medium Create a user with SIWE, then try to associate a real email address. The temporary email address created for new users is not a real email address, which could cause problems if the user later wants to associate a real email with their account.
Missing Tests High N/A The tests don't cover all critical aspects of the plugin, such as user creation, nonce deletion, and the emailDomainName option.
Potential Replay Attack High N/A Although the plugin uses a nonce mechanism, there is no test to ensure that used nonces are actually deleted from the database. A malicious user might be able to reuse a valid signature if the nonce is not properly deleted.

Comments? Email us.

@reslear
Copy link
Contributor
reslear commented May 8, 2025

Thank @rokitgg you for your hard work

@8times4 @b-bot Still disagree, this is not consistent with the whole library:

  1. there should be no additional packages neither view nor siwe nor ox (this should be user selectable).
  2. all functions should be customizable by the user, giving the user a choice

Imagine if we added only 1 provider to the plugin https://www.better-auth.com/docs/concepts/email

@rokitgg
Copy link
Author
rokitgg commented May 8, 2025

I certainly think @reslear makes a really strong point here.

If i had no other option but to add any deps (as peer) my personal choice would be siwe since it's such a tiny package in comparison and does the job we need here pretty well, however I do agree with giving users - in this case developers - the choice to bring their own implementation via viem, ox or whatever choice they wish to.

@b-bot
Copy link
b-bot commented May 9, 2025

Yeah let's go with typing the function and leaving implementation to the user to pass in the config 👌🏼

As a final note (and why I mentioned Anonymous plugin) we need to handle account linking. ie. user signs in with google while signed in with eth and it should replace the fake email. This would allow retrieval of the account through multiple methods.

@reslear
Copy link
Contributor
reslear commented May 9, 2025

Next point - it will be necessary to check how it works when a user has many addresses, and maybe switching or linking

… and message verification through plugin options

- Removed direct dependency on 'siwe' package.
- Introduced 'generateSiweNonce' and 'verifySiweMessage' as user functions
- Updated tests to reflect changes in nonce handling and message verification logic (WIP)
Copy link
jazzberry-ai bot commented May 10, 2025

Bug Report

Name Severity Example test case Description
Insecure Nonce Generation High Reuse a valid signature with the same nonce. The generateSiweNonce function is not secure, allowing for replay attacks. If a predictable or reused nonce is generated, an attacker can reuse a valid signature to bypass verification and gain unauthorized access.

Comments? Email us.

@rokitgg
Copy link
Author
rokitgg commented May 10, 2025

Alright so just removed the siwe dependency and refactored related functions into being plugin options - for the most part just a mock but surely moving in the right direction

Where I'm a bit lost is on how to write exhaustive tests for this plugin - If anyone is willing to share a bit of light there I would very much appreciate that!

@b-bot
Copy link
b-bot commented May 11, 2025

@rokitgg You're going to want to finalise everything before writing test cases but I would say that because it's more of an open implementation we can mock most things. I don't know the policy about adding packages for testing (since dev deps aren't shipped in bundle) but this exists: https://wagmi.sh/react/api/connectors/mock

Use AI to gen some cases after finishing everything and we can look for gaps.

There's also examples of how SIWE is testing to give you ideas: https://github.com/spruceid/siwe/blob/main/packages/siwe/lib/client.test.ts

PS. I thought of one more thing we might want to add to config.

An anonymous field defaulting to true. If not it requires an email to be passed into the sign in call with the payload. Because Better Auth is so dependant on email this is a good idea and will help with notifications and account linking.

Edit:

@rokitgg
Copy link
Author
rokitgg commented May 13, 2025

Thank you so much for the suggestions @b-bot - already implemented the anonymous flag

Regarding tests, the issue here when using the mock feature from wagmi it's that I would be likely falling into the same issue again of having wagmi as a dep, so that's concerning still, but really nice that you pointed that out.

Will be taking a closer look at the siwe tests you mentioned there, that's probably quite helpful.

@b-bot
Copy link
b-bot commented May 13, 2025

@rokitgg looking good here, excited to use this!
I can pull this to test and write the docs if you like?

export const schema = {
user: {
fields: {
walletAddress: {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regarding this, wouldn't it possibly make sense to call it evmAddress?
only reason i'm asking is because SIWS (Sign In With Solana) could not use publicKey as it being too generic, - but solAddress would not be so "streamlined" compared to walletAddress.

(This is to future-proof as I'm planning to add SIWS later)

Copy link
@b-bot b-bot May 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@8times4 wouldn't that be another plugin as this is tightly coupled to ENS etc?
(I don't know SIWS)

Copy link
@8times4 8times4 May 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct @b-bot although SIWX exists, that would be overkill most likely.

SIWS has SNS instead of ENS, only proposing this evmAddress change due to: #2579 (comment)
so that it's somewhat similar/understandable in the schema which does what if someone uses both SIWE and SIWS

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't rlly have any strong views on this so absolutely open to rename the field into whatever feels the most convenient for everyone - naming things is truly hard and it shows

@rokitgg
Copy link
Author
rokitgg commented May 13, 2025

@rokitgg looking good here, excited to use this! I can pull this to test and write the docs if you like?

Sure! Go ahead whenever u wish to.

@vimtor
Copy link
vimtor commented May 14, 2025

Hey! Just chiming in to suggest that the plugin should probably store the checksummed version of the wallet address instead of the raw input. Not all wallets send the address properly checksummed, and this could lead to inconsistencies when trying to match the user given a wallet.

@rokitgg
Copy link
Author
rokitgg commented May 15, 2025

@vimtor thanks for your suggestion, totally missed that one! Will absolutely look to implement the checksum parsing.

The issue I've found here is that the hashing algorithm Keccak-256 is not natively supported by node:crypto, so this will led us into adding a peer-dep to the plugin, which is not ideal even tho the keccak package is so tiny.

node:crypto supports sha3-256 which is very similar, but if we use sha3-256 instead of Keccak-256 for Ethereum addresses, we will get the wrong checksum.

If we want strict EIP-55 compliance, we need a Keccak256 implementation somehow - happy to discuss the go to way for this.

For reference:

https://ethereum.stackexchange.com/questions/550/which-cryptographic-hash-function-does-ethereum-use

https://eips.ethereum.org/EIPS/eip-55

As defined in the EIP-55 reference (link above), this is the appropriate implementation:

const createKeccakHash = require('keccak')

function toChecksumAddress (address) {
  address = address.toLowerCase().replace('0x', '')
  var hash = createKeccakHash('keccak256').update(address).digest('hex')
  var ret = '0x'

  for (var i = 0; i < address.length; i++) {
    if (parseInt(hash[i], 16) >= 8) {
      ret += address[i].toUpperCase()
    } else {
      ret += address[i]
    }
  }

  return ret
}

@reslear
Copy link
Contributor
reslear commented May 15, 2025

@rokitgg
viem use small module https://github.com/paulmillr/noble-hashes
we can add Tree-shakeable keccak to https://github.com/better-auth/utils

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Siwe support
5 participants
0