Next.js Discord

Discord Forum

Next.js 13 authentication with metamask. Next-auth cant genereate csrf token on the server

Answered
Atlantic menhaden posted this in #help-forum
Open in Discord
Atlantic menhadenOP
I am having trouble authenticating with metamask, specifically generating csrf token. I followed this tutorial (https://dev.to/abhikbanerjee99/nextjs-13-using-next-auth-the-web3-way-17kg) and everything works fine.
My env: NEXTAUTH_URL="http://localhost:3000"

When i click sign message i get a valid session back, but in the terminal i get this error:

[next-auth][warn][DEBUG_ENABLED]  
https://next-auth.js.org/warnings#debug_enabled
[next-auth][error][CLIENT_FETCH_ERROR]  
https://next-auth.js.org/errors#client_fetch_error Unexpected token E in JSON at position 0 {
  error: {
    message: 'Unexpected token E in JSON at position 0',
    stack: 'SyntaxError: Unexpected token E in JSON at position 0\n' +
      '    at JSON.parse (<anonymous>)\n' +
      '    at parseJSONFromBytes (node:internal/deps/undici/undici:6571:19)\n' +
      '    at successSteps (node:internal/deps/undici/undici:6545:27)\n' +
      '    at node:internal/deps/undici/undici:1211:60\n' +
      '    at node:internal/process/task_queues:140:7\n' +
      '    at AsyncResource.runInAsyncScope (node:async_hooks:203:9)\n' +
      '    at AsyncResource.runMicrotask (node:internal/process/task_queues:137:8)\n' +
      '    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)',
    name: 'SyntaxError'
  },
  url: 'http://localhost:3000/api/auth/csrf',
  message: 'Unexpected token E in JSON at position 0'
}

-  ┌ POST /api/auth/callback/web3 200 in 270ms
   │ 
   └──── POST http://localhost:3000/api/auth/csrf 400 in 82ms (cache: MISS)


On further investigation i discovered that getcsrwToken({req}) doesnt work:

const result = await siwe.verify({
    signature: credentials.signedMessage,
    nonce: await getCsrfToken({req})
});


It always returns undefined, and even if i put "nonce: undefined" it still lets me authorize and return session. I dont think that is right. Any help please?
Answered by Atlantic menhaden
bum
View full answer

302 Replies

you using app dir?
the request isnt going to work like you have it
const credentials = await request.json();
  const req: CtxOrReq = {
    req: {
      headers: {
        cookie: request.headers.get('cookie')
      }
    }
  }
  try {
    const message = new SiweMessage(JSON.parse(credentials.message || "{}"))
    // const nextAuthUrl = new URL(process.env.NEXTAUTH_URL)
    const nonce = await getCsrfToken(req);

    const fields =  await message.validate(credentials.signature)
    if (fields.nonce !== nonce) {
      return new NextResponse(JSON.stringify({ success: false, error: 'Nonce mismatch' }), { status: 400 });
    }
@Atlantic menhaden
@DirtyCajunRice | AppDir you using app dir?
Atlantic menhadenOP
yes
@Atlantic menhaden yes
i gave you a working example of the way you need to restructure your req
@DirtyCajunRice | AppDir i gave you a working example of the way you need to restructure your req
Atlantic menhadenOP
i see, thank you, am testing it now
that said
if you switch to viem, you dont have to use siwe at all
@DirtyCajunRice | AppDir if you switch to viem, you dont have to use siwe at all
Atlantic menhadenOP
tell me more haha
ethers is fucked
Atlantic menhadenOP
isnt viem included in wagmi?
yep
@DirtyCajunRice | AppDir ethers is fucked
Atlantic menhadenOP
why is that?
@DirtyCajunRice | AppDir yep
Atlantic menhadenOP
then i have it?
@Atlantic menhaden then i have it?
then why the hell you using siwe? haha
lemme get you my viem way
@DirtyCajunRice | AppDir then why the hell you using siwe? haha
Atlantic menhadenOP
haha bc it was in the tutorial 😂
i am stil learning
const data = await request.text();

    const json = JSON.parse(data);
    if (!('address' in json) || !('signature' in json) || !('message' in json)) {
      return NextResponse.json({ error: 'Malformed Request' }, { status: 400 })
    }
    const recoveredAddress = await recoverMessageAddress({
      message: json.message,
      signature: json.signature
    });
    if (recoveredAddress !== json.address) {
      return NextResponse.json({ error: 'Invalid Signature' }, { status: 400 })
    }
this is the api route part
lemme grab the sign part
const UnlockableContent = () => {
  const { address, isConnected } = useAccount();
  const { chain } = useNetwork();
  const [codes, setCodes] = useState<{ tokenId: number, code: string }[]>([]);

  const handleSignedMessage = async (signature: string) => {
    const r = await fetch('/api/reveal', {
      method: 'POST',
      body: JSON.stringify({
        address,
        signature,
        message: `I verify that i own ${address} to reveal the discount codes`
      }),
    })
    if (r.ok) {
      const data = await r.json();
      if (data?.results?.length > 0) {
        setCodes(data.results)
      }
    }
  }

  const { signMessage } = useSignMessage({
    message: `I verify that i own ${address} to reveal the discount codes`,
    onSuccess: handleSignedMessage
  })
tadaaaa
Atlantic menhadenOP
niice
const data = await request.text();
this part, where do i get request?
the api route
Atlantic menhadenOP
async authorize(credentials, req) {
GET(request: NextRequest)
Atlantic menhadenOP
Unresolved function or method text()
wat
show me what you have put as your handler
Atlantic menhadenOP
oh. you are doing it inside of the damn provider
Atlantic menhadenOP
this is api/auth/[...nextauth]/route.js
yeah. i made my own route handler. thats ok. we can do it this way too
req.text is for a normal route handler
Atlantic menhadenOP
can i show you the whole route?
you need to modify it to use the data you are gonna pass from credentials
Atlantic menhadenOP
okay
what do i need to do
you dont need any of the NextResponse bits
you dont need the json parse or the res text bit
and instead of json.measage and json.signature
you can do credentials.message and credentials.signedMessage
ezpz
Atlantic menhadenOP
instead of nextResponse do i throw error?
iirc yes… whats the original example do if its a bad signature?
Atlantic menhadenOP
throws error
then yeah thats what the provider expects
Atlantic menhadenOP
import NextAuth from "next-auth/next";
import CredentialsProvider from "next-auth/providers/credentials";
import { getCsrfToken } from "next-auth/react";


export const authOptions = {
    providers: [
        CredentialsProvider({
            id: "web3",
            name: "web3",
            credentials: {
                message: { label: "Message", type: "text" },
                signedMessage: { label: "Signed Message", type: "text" },
            },
            async authorize(credentials, req) {
                if (!credentials?.signedMessage || !credentials?.message) {
                    return null;
                }

                try {
                    const siwe = new SiweMessage(JSON.parse(credentials?.message));
                    const result = await siwe.verify({
                        signature: credentials.signedMessage,
                        nonce: await getCsrfToken({req})
                    });

                    if (!result.success) throw new Error("Invalid Signature");

                    if (result.data.statement !== process.env.NEXT_PUBLIC_SIGNIN_MESSAGE)
                        throw new Error("Statement Mismatch");

                    if (new Date(result.data.expirationTime) < new Date())
                      throw new Error("Signature Already expired");

                    return {
                        id: siwe.address,
                    };
                } catch (error) {
                    console.log(error);
                    return null;
                }
            },
        }),
    ],
    session: { strategy: "jwt" },

    debug: process.env.NODE_ENV !== "production",

    secret: process.env.NEXTAUTH_SECRET,

    callbacks: {
        async session({ session, token }) {
            session.user.address = token.sub;
            session.user.token = token;
            return session;
        },
    },
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };
this is the whole route
recoverMessageAddress is from viem?
yep
Atlantic menhadenOP
where do i get json address?
i mean what do i replace it with
you should send that as an additional param in credentials tbh
Atlantic menhadenOP
okay
and if you wanna get real fancy also send the csrf nonce and validate it inside the message
but thats advanced 😅
Atlantic menhadenOP
haha well i guess i will, when i get the basics going
i really appreciate your help, i have been busting my head for a while now with this
sokay man. crypto shitheads are bad about sharing info
which is so anti crypto 😂
Atlantic menhadenOP
this is where i call signIn from next-auth
you made your own route, yes? how do i add address here?
ok so we will be changing shit
Atlantic menhadenOP
yeea haha
make your message via a format
instead of via new siwe
Atlantic menhadenOP
okay yes so siwe is gone
so like…
const message = [`domain: ${window.location.host}`, `uri: ${window.location.origin}`, etc etc].join(`/n`)
(im typing from my phone so i gotta save effort where i can haha)
Atlantic menhadenOP
haha no problem
the signedMessage should come from the hook i gave you above
then in your signIn call, add a new address field, and a nonce field
@Atlantic menhaden should i make an array [] or object {}
you are building a string. that string is what will show up in metamask
the array is just for easy formatting
i join it with a newline at the end
Atlantic menhadenOP
okay i understand
remember that your hook for useSignMessage isnt going inside of this function you are typing in
its going outside of the function but in the component
@Atlantic menhaden ofcourse
you never know with the people in this discord man…
@DirtyCajunRice | AppDir you never know with the people in this discord man…
Atlantic menhadenOP
no, thank you, i love how you explain stuff
i know some things but other basics i dont haha so its great that you explain everything :)
with csrf should i leave it ?
you can add it
doesnt hurt
Atlantic menhadenOP
okay
you will learn how to validate it later
for now its just extra text.
Atlantic menhadenOP
sure
the message isnt going in this function anymore though
because you need it in the hook
so lets move it to a function
Atlantic menhadenOP
signMessage from wagmi right
const makeMessage = (address: Address, csrf: string) => typeof window === "undefined" ? "" : yourarrayjoinhere
then you can use that function in both the useSignMessage, as well as the signIn() function
Atlantic menhadenOP
replace the 2 variables
Atlantic menhadenOP
is that right?
yea
account.address is now just address, same with csrf
Atlantic menhadenOP
nice
ah you are using js not ts?
baaadddd aljo
jkjk
Atlantic menhadenOP
hahahahhaa
ok moving on haha
Atlantic menhadenOP
i know i know
ok so show me how you placed the hook
Atlantic menhadenOP
the signMessage hook?
this dude… gm wagmi friends hahahaha
replace that string with the function we just made
Atlantic menhadenOP
aw geez
?
Atlantic menhadenOP
i wil lbutcher this
why
show it
Atlantic menhadenOP
you will be mad :p
should just be message: makeMessage(account.address, someshit)
Atlantic menhadenOP
yeah thats good
Atlantic menhadenOP
but cant await
what do you need await for
Atlantic menhadenOP
bc i am outside of async function now right?
@DirtyCajunRice | AppDir what do you need await for
Atlantic menhadenOP
csrf
ohhhh
then keep csrf blank for now
Atlantic menhadenOP
okay
(account.address, “”)
Atlantic menhadenOP
yep got it
ultimate fail. its ok tho
so now your onSuccess
Atlantic menhadenOP
wait
so
now i dont have to put message in right?
just sign it?
where is that from
thats your old shit
blow that caca away
Atlantic menhadenOP
hahaha
okay okay
instead you will call signMessage()
wait
wait
lemme go look at my code
Atlantic menhadenOP
sure
thanks
ohhh
you threw me off by not destructuring the hook
Atlantic menhadenOP
okay so how do you want it destructured haha
like i sent it
const { signMessage }
Atlantic menhadenOP
or if you want the async version
Atlantic menhadenOP
oh okay
const { signMessageAsync }
@DirtyCajunRice | AppDir or if you want the async version
Atlantic menhadenOP
idk what is the difference
@Atlantic menhaden idk what is the difference
one is… asynchronous
😂
@DirtyCajunRice | AppDir one is… asynchronous
Atlantic menhadenOP
yeah okaaaay hahah
but if you await it...
then its the same.. nevermind
the lightbulb has turned on ladies and gentlemen
😂
Atlantic menhadenOP
😂
so would you use async one or no?
try not to. if it does weird shit then try the other
its all code dependant
Atlantic menhadenOP
okay good
so do i call sign message now?
yep. dont need to pass any args to it
just ()
Atlantic menhadenOP
exactly
Atlantic menhadenOP
i destructured haha
you dont even need to store it
since the result is handled by onSuccess
Atlantic menhadenOP
good point
@DirtyCajunRice | AppDir since the result is handled by onSuccess
Atlantic menhadenOP
here i call sign in right?
after validating
o right
you are using cred provider
so yeah just call it
@DirtyCajunRice | AppDir you are using cred provider
Atlantic menhadenOP
what would you use?
cred provider is fine. i just personally keep my “account” auth, and “wallet” auth separate for simplicity
that way users can log in with like… discord… then attach a wallet.. but not need to always use the wallet
its all use-case based
Atlantic menhadenOP
ahaa okay
for me i just need wallet
anonymous kinda like
“anonymous” he said.
mr robot he said
😂
Atlantic menhadenOP
hahahaa
well you know what i mean
ammm
ikik just messin
Atlantic menhadenOP
so what do i pass for signin?
also
with () or without?
without
you are passing the function definition
show me the handle func
Atlantic menhadenOP
i dont know if its going to like that its async
the param is “data”
which is the signed message
message: is the same as the hook. makeMessage(……
Atlantic menhadenOP
whoahwhoah
you cant go changing the key sir
signedMessage: data
😂
Atlantic menhadenOP
ups hahahaha
and you want the address too remember
Atlantic menhadenOP
yep
so add address: account.address
Atlantic menhadenOP
nice.
Atlantic menhadenOP
👍
if it isnt pissy about the async func then you should be good to go
Atlantic menhadenOP
what about response.error?
next auth already has built in error handling
so you dont need any of that
nor do you need to save the signIn
Atlantic menhadenOP
okay great
now validate in the route?
you are already doing that in the provider
so now just test it
Atlantic menhadenOP
yeah i need to clean the route of siwe haha
show me that again
Atlantic menhadenOP
like you said earlier
the cred provider
Atlantic menhadenOP
now its a bit fucked
thats how we like it
janky and half assed
Atlantic menhadenOP
hahahaa
add address to the credentials definition
and also make sure it exists like you do for signedMessage and message
Atlantic menhadenOP
yeah nice
Atlantic menhadenOP
you need to verify the message before you try to recover it
Atlantic menhadenOP
okay
const valid = await verifyMessage({ 
  address: credentials.address,
  message: makeMessage(credentials.address, "")
  signature: credentials.signedMessage,
})
then the obvious if not valid throw error
Atlantic menhadenOP
should i export make message function?
1000%
Atlantic menhadenOP
okay
after you verify the message then you recover the address
and make sure the recovered address === credentials.address
if it does… you are “validated”
and can blow away the remaining siwe bullshit at the bottom
Atlantic menhadenOP
niiiice
here is complaining that i am ignoring promise
should i just leave it or await?
.then()
add that at the end
Atlantic menhadenOP
okay
will make the linter gods happy
Atlantic menhadenOP
hahah okay
this ok?
yep
oh
derp
i forgot the message was in the damn send
you dont need makeMessage
can just do credentials.message there too
Atlantic menhadenOP
ok ok
so if the recovered address is the same as cred.address then we are validated?
yep!
later you can add nonce validation but leave that for another day
0.0000001% of people know how to manipulate csrf nonce to begin with
Atlantic menhadenOP
bum
Answer
ond you probably :p
Atlantic menhadenOP
thank you so much for your help
i love you :p
no problem dude haha
@Atlantic menhaden make sure you mark it as solved so people dont have to read through this bible of a chat history
hope to see you again man :p
i will be back with more stupid questions hahah