nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
24.11k stars 3.34k forks source link

Web3 Provider #3292

Closed Pruxis closed 2 years ago

Pruxis commented 2 years ago

Description 📓

I was wondering if there are plans to include Web3 wallet connections as part of the providers? If this is something that you'd like to scope in next-auth then I'd be happy to contribute.

Contributing 🙌🏽

Yes, I am willing to help implement this feature in a PR

balazsorban44 commented 2 years ago

I have to admit, I have zero knowledge of what Web 3 even is, so probably not in the short term.

Right now I can only see its a controversial topic on Twitter amongst developers.

I have no idea what that would mean for next-auth, what would be required/expected from us, etc.

For starters, could you link to some useful resources that starts explaining at absolute zero? What is the goal of Web 3, what is a wallet, etc?

Pruxis commented 2 years ago

Resources:

Here's a dated but simple repository of how wallet authentication would look like: https://github.com/vanbexlabs/web3-auth

A large portion of the web3 community currently builds dApps that mainly run on-top of blockchain technologies. However I believe that there's more and more need to combine those applications with "web2" backends and that there's very few proper resources that use best practices to facilitate this.

balazsorban44 commented 2 years ago

It would be a long way for me to advocate for best practices of Web3 auth, as I barely started out with OAuth, and there is much to learn!

I also don't know what dApps are and have never really looked into blockchain either.

As this project is mainly maintained by me, I am afraid I won't have the capacity for a very long time to look into this.

But I encourage you to dive into the source code, learn about it, and see if you can get what you want working, and see how that goes.

I would like to learn what are the true benefits of this approach? Who is the userbase? Why isn't the current way working for them?

Only relying on the limited knowledge of my social media exposure, I jump on all of this a bit skeptical, please forgive me for that.

Pruxis commented 2 years ago

@balazsorban44 I'll make a sample with CredentialsProviders, perhaps some documentation or a blogpost could help those in need of Web3 Auth. Then there is no commitment and deep knowledge required on next-auth.

balazsorban44 commented 2 years ago

Let's see how that goes! if not complicated and we can just add this as an extension of the Credentials provider as it's standalone provider, that would be great.

Pruxis commented 2 years ago

So I made a sample real quick, I'll find some time tomorrow perhaps write a blogpost about it. This allows you to use an etherium wallet to authenticate yourself.

The really cool thing about next-auth is the csrfToken that it's a unique token for your session so you can actuality use it as signing mechanism to verify that the connected wallet is properly authenticated :)

Frontend sample:

import Web3 from 'web3';
import { useCallback, useEffect, useState } from 'react';

export let web3: Web3 | undefined;

interface State {
  started: boolean;
  connected: boolean;
}

interface MetamaskButtonProps {
  csrfToken: string;
}

const LoginButton = (props: MetamaskButtonProps) => {
  const [state, setState] = useState<State>({ started: false, connected: false });

  const checkConnectedWallet = useCallback(() => {
    web3?.eth?.getAccounts((err, accounts) => {
      if (err != null) return setState(s => ({ ...s, started: true }));
      else if (!accounts.length) return setState(s => ({ ...s, started: true }));
      return setState({ started: true, connected: true });
    });
  }, [setState]);

  const signInWithWallet = useCallback(async () => {
    window.ethereum
      ?.request({ method: 'eth_requestAccounts' })
      // Returns an array of web3 addresses.
      .then(async (accounts: string[]) => {
        try {
          const signedCsrf = await window.ethereum?.request({
            method: 'personal_sign',
            params: ['0x' + props.csrfToken, accounts[0]],
          });
          const form = document.createElement('form');
          form.setAttribute('action', '/api/auth/callback/credentials');
          form.setAttribute('method', 'POST');
          // Add csrfToken
          const csfrInput = document.createElement('input');
          csfrInput.setAttribute('name', 'csrfToken');
          csfrInput.setAttribute('value', props.csrfToken);
          form.appendChild(csfrInput);
          const web3Address = document.createElement('input');
          web3Address.setAttribute('name', 'address');
          web3Address.setAttribute('value', accounts[0]);
          form.appendChild(web3Address);
          const signature = document.createElement('input');
          signature.setAttribute('name', 'signature');
          signature.setAttribute('value', signedCsrf);
          form.appendChild(signature);
          document.getElementsByTagName('body')[0].appendChild(form);
          form.submit();
          console.log('Submitted form');
        } catch (e) {}
      });
  }, [props.csrfToken]);

  useEffect(() => {
    web3 = new Web3(window.ethereum as any);
    checkConnectedWallet();
  }, []);

  if (!state.started || typeof window === 'undefined') return null;

  return (
    <div className="flex flex-col w-full h-full p-4">
      <button className="p-2 border-2 border-black rounded-sm max-w-sm" onClick={signInWithWallet}>
        Authenticate with Metamask
      </button>
    </div>
  );
};

export default LoginButton;

Provider:

CredentialsProvider({
  name: 'Web3',
  authorize: async (credentials: any, f) => {
    const tokenBuffer = ethUtil.toBuffer('0x' + credentials.csrfToken);
    const tokenHash = ethUtil.hashPersonalMessage(tokenBuffer);
    const signatureParams = ethUtil.fromRpcSig(credentials.signature);
    const publicKey = ethUtil.ecrecover(tokenHash, signatureParams.v, signatureParams.r, signatureParams.s);
    const addressBuffer = ethUtil.publicToAddress(publicKey);
    const address = ethUtil.bufferToHex(addressBuffer);
    if (address.toLowerCase() !== credentials.address?.toLowerCase()) return null;
    // Replace with db stuff here.
    const user = { id: 1, name: 'J Smith', email: 'jsmith@example.com', ...credentials };
    return user;
  },
  type: 'credentials',
  credentials: {},
})
kellvembarbosa commented 2 years ago

Hello,

I'm interested in helping, but I know little about the blockchain, do you have any updates to the above code?

I'm working on it, but I can't successfully pass ethUtil.toBuffer()

I'm using 'ethereumjs-util' can you specify the package you used?

================= Update question:

replace this line:

const tokenBuffer = ethUtil.toBuffer('0x' + credentials.csrfToken);

for this:

const tokenBuffer = Buffer.from('0x' + credentials.csrfToken);

now it worked for me

Pruxis commented 2 years ago

@kellvembarbosa I got some time this weekend to finalise this sample, as there's a bug when you first connect your wallet it doesnt properly succeed, however after refresh and resigning the token it does which is quite odd

I used ethereumjs-util@7.1.3 to use the sample here.

kellvembarbosa commented 2 years ago

@Pruxis it's true, I also realized that, in the code above, the user will need to perform two actions, As I understand it, one action is to 'activate' access to the account and then make the personal sign…

Thanks for your time, I'll keep an eye out when you update the code, thanks again.

RothNRK commented 2 years ago

@Pruxis really looking forward to your blog post. Thanks for the example code!

pepevflolio commented 2 years ago

@Pruxis love to see the complete example! I can't manage to make it work. Thanks a lot!

shunkakinoki commented 2 years ago

@Pruxis +1

soldev42 commented 2 years ago

Thanks @Pruxis. Love to see this to be integrated. web3 social platforms will need this.

kellvembarbosa commented 2 years ago

Hi,

This makes the code cleaner and easier to read.

First import:

import { utils } from "ethers"

and


CredentialsProvider({
  name: 'Web3',
  authorize: async (credentials: any, f) => {
    const nonce = '0x' + credentials.csrfToken;
    const address = utils.verifyMessage(nonce, credentials.signature);
    if (address.toLowerCase() !== credentials.address?.toLowerCase()) return null;
    //  create newUser or return existent user
    const user = { id: 1, name: 'J Smith', email: 'jsmith@example.com', ...credentials };
    return user;
  },
  type: 'credentials',
  credentials: {},
}) ´´´
RothNRK commented 2 years ago

@Pruxis Have you made any headway with this bug?

It looks like the csrfToken lags behind?

I have a page connect.js with a call to getServerSideProps:

export async function getServerSideProps(context) {
  return {
    props: {
      csrfToken: await getCsrfToken(context),
    },
  }
}

The token values change in the sequence:

1) Load http://localhost:3000/connect front-end 2) getServerSideProps -> props.csrfToken: 011bdcd... 3) signInWithWallet -> props. csrfToken: 011bdcd... 4)Click signInWithWallet button and sign MetaMask signature request

From [...nextauth].js 5) From CredentialsProvider authorize function -> credentials.csrfToken: 6c3263b.....

When you refresh http://localhost:3000/connect 5)signInWithWallet -> props. csrfToken: 6c3263b...

I replaced:

    const form = document.createElement('form');
    ...
    console.log('Submitted form');

with

signIn("web3", {callbackUrl: '/', address: accounts[0], csrfToken: props.csrfToken,  signature: signedCsrf})

because I was getting an error about POST to /api/auth/callback/credentials not being supported.

@balazsorban44 Would you have any idea why the token value passed into the signin method is not passed into the authorize method?

Thank you for any suggestion.

Edit: I also noticed that every time you refresh before your first MetaMask connect/signature attempt you will get a new/different csrfToken each time, while after your first attempt you will get a new csrfToken that remains the same after each refresh.

csadai commented 2 years ago

I have been using web3-react (https://github.com/NoahZinsmeister/web3-react) with Next.js successfully. Maybe we can think of an integration?

tmm commented 2 years ago

Sign-In with Ethereum could be a good option. I put together a guide for the wagmi library here.

Pruxis commented 2 years ago

I'm no longer active in the Web3 space, therefore I'll close this PR, if anyone else wants to pick this up feel free to re-open

matbee-eth commented 2 years ago

Sign-In with Ethereum could be a good option. I put together a guide for the wagmi library here.

I'm using wagmi so I'd love to see it integrated!

montanaflynn commented 2 years ago

I'd like to see this re-opened. To answer @balazsorban44's questions:

I would like to learn what are the true benefits of this approach? Who is the userbase? Why isn't the current way working for them?

The userbase is crypto adopters who value decentralization. With current next auth providers there's a single company who is the source of authn/authz. At any time and for any reason they could shut down, ban you, get hacked, etc...

With a web3 provider there would be no need to register with such a company, all you would need to do is generate a new wallet address which is free and available to anyone through a variety of means. This address can then be used to login to many different web3 "dApps" (decentralized apps). It's basically a public / private key system, where you have a private key that can be used to verify you own the public key address.

I think a few potential snags would be there is no OAuth involved and there would be no "profile" fields like username, image or email.

Linch1 commented 2 years ago

Hello guys i've wrapped a web3 auth web app with next auth, maybe this can helpfull

https://github.com/Linch1/Web3NextjsAuth

tmm commented 2 years ago

The SIWE team published this guide that uses next-auth and wagmi.

Jbry123 commented 1 year ago

So I made a sample real quick, I'll find some time tomorrow perhaps write a blogpost about it. This allows you to use an etherium wallet to authenticate yourself.

The really cool thing about next-auth is the csrfToken that it's a unique token for your session so you can actuality use it as signing mechanism to verify that the connected wallet is properly authenticated :)

Frontend sample:

import Web3 from 'web3';
import { useCallback, useEffect, useState } from 'react';

export let web3: Web3 | undefined;

interface State {
  started: boolean;
  connected: boolean;
}

interface MetamaskButtonProps {
  csrfToken: string;
}

const LoginButton = (props: MetamaskButtonProps) => {
  const [state, setState] = useState<State>({ started: false, connected: false });

  const checkConnectedWallet = useCallback(() => {
    web3?.eth?.getAccounts((err, accounts) => {
      if (err != null) return setState(s => ({ ...s, started: true }));
      else if (!accounts.length) return setState(s => ({ ...s, started: true }));
      return setState({ started: true, connected: true });
    });
  }, [setState]);

  const signInWithWallet = useCallback(async () => {
    window.ethereum
      ?.request({ method: 'eth_requestAccounts' })
      // Returns an array of web3 addresses.
      .then(async (accounts: string[]) => {
        try {
          const signedCsrf = await window.ethereum?.request({
            method: 'personal_sign',
            params: ['0x' + props.csrfToken, accounts[0]],
          });
          const form = document.createElement('form');
          form.setAttribute('action', '/api/auth/callback/credentials');
          form.setAttribute('method', 'POST');
          // Add csrfToken
          const csfrInput = document.createElement('input');
          csfrInput.setAttribute('name', 'csrfToken');
          csfrInput.setAttribute('value', props.csrfToken);
          form.appendChild(csfrInput);
          const web3Address = document.createElement('input');
          web3Address.setAttribute('name', 'address');
          web3Address.setAttribute('value', accounts[0]);
          form.appendChild(web3Address);
          const signature = document.createElement('input');
          signature.setAttribute('name', 'signature');
          signature.setAttribute('value', signedCsrf);
          form.appendChild(signature);
          document.getElementsByTagName('body')[0].appendChild(form);
          form.submit();
          console.log('Submitted form');
        } catch (e) {}
      });
  }, [props.csrfToken]);

  useEffect(() => {
    web3 = new Web3(window.ethereum as any);
    checkConnectedWallet();
  }, []);

  if (!state.started || typeof window === 'undefined') return null;

  return (
    <div className="flex flex-col w-full h-full p-4">
      <button className="p-2 border-2 border-black rounded-sm max-w-sm" onClick={signInWithWallet}>
        Authenticate with Metamask
      </button>
    </div>
  );
};

export default LoginButton;

Provider:

CredentialsProvider({
  name: 'Web3',
  authorize: async (credentials: any, f) => {
    const tokenBuffer = ethUtil.toBuffer('0x' + credentials.csrfToken);
    const tokenHash = ethUtil.hashPersonalMessage(tokenBuffer);
    const signatureParams = ethUtil.fromRpcSig(credentials.signature);
    const publicKey = ethUtil.ecrecover(tokenHash, signatureParams.v, signatureParams.r, signatureParams.s);
    const addressBuffer = ethUtil.publicToAddress(publicKey);
    const address = ethUtil.bufferToHex(addressBuffer);
    if (address.toLowerCase() !== credentials.address?.toLowerCase()) return null;
    // Replace with db stuff here.
    const user = { id: 1, name: 'J Smith', email: 'jsmith@example.com', ...credentials };
    return user;
  },
  type: 'credentials',
  credentials: {},
})

https://twitter.com/JonBry_0xDreamr/status/1576992188692852737

tipped you some BAT for contributing

BlockLM commented 1 year ago

Anyone had any luck with Web3Auth as a Provider? Would it follow the same methods as @Pruxis ?

Yes, there's convoluted methods in the wild where we have to use Auth0 as a passthrough, but I would prefer to minimize risk and abstraction layers.

I'm looking to end up with Prisma as an oracle to match Web3Auth (currently experimenting via T3 App)

saurabhsri108 commented 1 year ago

I think we can now look into pre-built providers for ETH, MATIC, SOL; for now. The providers take care of necessary auth but the user provides his own rpc network (Alchemy, Infura, etc).

Maybe I can look into it if time permits (no commitment).

montanaflynn commented 1 year ago

Why would we need a network for signature verification?

XahidEx commented 1 year ago

I'm also looking for something stable ❤️🌼

wade-liwei commented 4 months ago

I'm also looking for something stable ❤️🌼