hello, I’m trying to get authentication working with nextjs, next-auth and moralis following the docs here:
https://docs.moralis.io/docs/sign-in-with-metamask , as well as the new tutorial that went on youtube a few days ago covering the same topic.
I tried implementing this in my own project and ended up with errors during the authentication stage so I’ve stripped back to just following the tutorial exactly step by step with a fresh project and I have the same problem. I have also just downloaded the github repo with the example project and still get the same errors with that. So the following is all true given that I am using the exact same code as the docs/sample project with no modifications. I can confirm I have implemented my own .env.local files to provide the API key, nextauth secret, app domain and next-auth url. (https://github.com/MoralisWeb3/demo-apps/tree/main/nextjs_moralis_auth)
The code seems to be working up until the next-auth step where the async function authenticate(credentials) {} is called. I get thrown into the error case here and can confirm with a console.log() there.
What happens, is I can click to sign in with metamask. I will get prompted with metamask with the correct message being provided in the window, once I click on ‘sign’, I am hit with an error:
TypeError: Cannot read properties of null (reading 'auth')
I’ve attached below a screenshot of the error as well as full console print-out during the error.
When I first tried running the project I was getting errors from next-auth when installing dependancies that it is not compatible with node versions above ^16.13.0. I had the newest 18.7.0 installed so I used nvm to roll back to 16.13.0 to try. This allowed me to install dependancies without error but I still get the same behaviour when trying the app. I am also an an M1 Mac, if that makes any difference.
edit - I have now tried re-building the project with vscode and terminal launched with Rosetta to try with x86 arch instead of arm64, I get the same result.
My searching around the internet shows some other people having to do work-arounds with next-auth on new node versions such as creating new session wrappers that intercept all axios calls to inject the session token inside each request/response. I tried implementing something similar but wasn’t able to make it work.
It seems others are able to use this workflow though so I’m hoping someone can help me zone in on what’s not working.
error log:
MoralisError [Moralis SDK Core Error]: [C0006] Request failed with status 400: Request failed with status code 400
at RequestController.makeError (/Volumes/DEV/moralis_auth/node_modules/@moralisweb3/core/lib/controllers/RequestController.js:137:20)
at RequestController.<anonymous> (/Volumes/DEV/moralis_auth/node_modules/@moralisweb3/core/lib/controllers/RequestController.js:118:38)
at step (/Volumes/DEV/moralis_auth/node_modules/@moralisweb3/core/lib/controllers/RequestController.js:44:23)
at Object.throw (/Volumes/DEV/moralis_auth/node_modules/@moralisweb3/core/lib/controllers/RequestController.js:25:53)
at rejected (/Volumes/DEV/moralis_auth/node_modules/@moralisweb3/core/lib/controllers/RequestController.js:17:65)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
isMoralisError: true,
code: 'C0006',
details: {
status: 400,
request: ClientRequest {
_events: [Object: null prototype],
_eventsCount: 7,
_maxListeners: undefined,
outputData: [],
outputSize: 0,
writable: true,
destroyed: false,
_last: true,
chunkedEncoding: false,
shouldKeepAlive: false,
maxRequestsOnConnectionReached: false,
_defaultKeepAlive: true,
useChunkedEncodingByDefault: true,
sendDate: false,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
_contentLength: null,
_hasBody: true,
_trailer: '',
finished: true,
_headerSent: true,
_closed: false,
socket: [TLSSocket],
_header: 'POST /challenge/verify/evm HTTP/1.1\r\n' +
'Accept: application/json, text/plain, */*\r\n' +
'Content-Type: application/json\r\n' +
'x-moralis-platform: JS SDK\r\n' +
'x-moralis-platform-version: 2.0.1\r\n' +
'x-moralis-build-target: node\r\n' +
'x-api-key: [redacted]\r\n' +
'User-Agent: axios/0.27.2\r\n' +
'Content-Length: 446\r\n' +
'Host: auth-api.do-prod-1.moralis.io\r\n' +
'Connection: close\r\n' +
'\r\n',
_keepAliveTimeout: 0,
_onPendingData: [Function: nop],
agent: [Agent],
socketPath: undefined,
method: 'POST',
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
path: '/challenge/verify/evm',
_ended: true,
res: [IncomingMessage],
aborted: false,
timeoutCb: null,
upgradeOrConnect: false,
parser: null,
maxHeadersCount: null,
reusedSocket: false,
host: 'auth-api.do-prod-1.moralis.io',
protocol: 'https:',
_redirectable: [Writable],
[Symbol(kCapture)]: false,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: [Object: null prototype],
[Symbol(kUniqueHeaders)]: null
},
response: {
status: 400,
statusText: 'Bad Request',
headers: [Object],
config: [Object],
request: [ClientRequest],
data: [Object]
}
},
[cause]: [AxiosError: Request failed with status code 400] {
code: 'ERR_BAD_REQUEST',
config: {
transitional: [Object],
adapter: [Function: httpAdapter],
transformRequest: [Array],
transformResponse: [Array],
timeout: 10000,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
maxBodyLength: -1,
env: [Object],
validateStatus: [Function: validateStatus],
headers: [Object],
url: 'https://auth-api.do-prod-1.moralis.io/challenge/verify/evm',
params: {},
method: 'post',
data: '{"message":"https://www.jasio.io wants you to sign in with your Ethereum account:\\n0xe044881312bd839409a017c5846d9eE33896059c\\n\\nPlease sign this message to confirm your identity.\\n\\nURI: http://localhost:3000\\nVersion: 1\\nChain ID: 4\\nNonce: jalxs9sl6rnq9BS55\\nIssued At: 2022-08-23T23:15:48.388Z","signature":"0x680db9883f198b33b69a4c930f8715a3111df2bc1d55793e7d99dcf90dd3356844906ccc78d46bc22580abc4a096611e2931074f8ba78130591524d98f8b2fb51c"}'
},
request: ClientRequest {
_events: [Object: null prototype],
_eventsCount: 7,
_maxListeners: undefined,
outputData: [],
outputSize: 0,
writable: true,
destroyed: false,
_last: true,
chunkedEncoding: false,
shouldKeepAlive: false,
maxRequestsOnConnectionReached: false,
_defaultKeepAlive: true,
useChunkedEncodingByDefault: true,
sendDate: false,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
_contentLength: null,
_hasBody: true,
_trailer: '',
finished: true,
_headerSent: true,
_closed: false,
socket: [TLSSocket],
_header: 'POST /challenge/verify/evm HTTP/1.1\r\n' +
'Accept: application/json, text/plain, */*\r\n' +
'Content-Type: application/json\r\n' +
'x-moralis-platform: JS SDK\r\n' +
'x-moralis-platform-version: 2.0.1\r\n' +
'x-moralis-build-target: node\r\n' +
'x-api-key: [redacted]\r\n' +
'User-Agent: axios/0.27.2\r\n' +
'Content-Length: 446\r\n' +
'Host: auth-api.do-prod-1.moralis.io\r\n' +
'Connection: close\r\n' +
'\r\n',
_keepAliveTimeout: 0,
_onPendingData: [Function: nop],
agent: [Agent],
socketPath: undefined,
method: 'POST',
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
path: '/challenge/verify/evm',
_ended: true,
res: [IncomingMessage],
aborted: false,
timeoutCb: null,
upgradeOrConnect: false,
parser: null,
maxHeadersCount: null,
reusedSocket: false,
host: 'auth-api.do-prod-1.moralis.io',
protocol: 'https:',
_redirectable: [Writable],
[Symbol(kCapture)]: false,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: [Object: null prototype],
[Symbol(kUniqueHeaders)]: null
},
response: {
status: 400,
statusText: 'Bad Request',
headers: [Object],
config: [Object],
request: [ClientRequest],
data: [Object]
}
}
}
package.json
{
"name": "nextjs_moralis_auth",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"axios": "^0.27.2",
"ethers": "^5.6.9",
"moralis": "2.0.1",
"next": "^12.2.4",
"next-auth": "^4.10.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"wagmi": "^0.6.1"
}
}
/signin.jsx
import { MetaMaskConnector } from 'wagmi/connectors/metaMask';
import { signIn } from 'next-auth/react';
import { useAccount, useConnect, useSignMessage, useDisconnect } from 'wagmi';
import { useRouter } from 'next/router';
import axios from 'axios';
function SignIn() {
const { connectAsync } = useConnect();
const { disconnectAsync } = useDisconnect();
const { isConnected } = useAccount();
const { signMessageAsync } = useSignMessage();
const { push } = useRouter();
const handleAuth = async () => {
if (isConnected) {
await disconnectAsync();
}
const { account, chain } = await connectAsync({ connector: new MetaMaskConnector() });
const userData = { address: account, chain: chain.id, network: 'evm' };
const { data } = await axios.post('/api/auth/request-message', userData, {
headers: {
'content-type': 'application/json',
},
});
const message = data.message;
const signature = await signMessageAsync({ message });
// redirect user after success authentication to '/user' page
const { url } = await signIn('credentials', { message, signature, redirect: false, callbackUrl: '/user' });
/**
* instead of using signIn(..., redirect: "/user")
* we get the url from callback and push it to the router to avoid page refreshing
*/
push(url);
};
return (
<div>
<h3>Web3 Authentication</h3>
<button onClick={() => handleAuth()}>Authenticate via Metamask</button>
</div>
);
}
export default SignIn;
/_app.js
import { createClient, configureChains, defaultChains, WagmiConfig } from 'wagmi';
import { publicProvider } from 'wagmi/providers/public';
import { SessionProvider } from 'next-auth/react';
const { provider, webSocketProvider } = configureChains(defaultChains, [publicProvider()]);
const client = createClient({
provider,
webSocketProvider,
autoConnect: true,
});
function MyApp({ Component, pageProps }) {
return (
<WagmiConfig client={client}>
<SessionProvider session={pageProps.session} refetchInterval={0}>
<Component {...pageProps} />
</SessionProvider>
</WagmiConfig>
);
}
export default MyApp;
/api/auth/[…nextauth].js
import CredentialsProvider from 'next-auth/providers/credentials';
import NextAuth from 'next-auth';
import Moralis from 'moralis';
export default NextAuth({
providers: [
CredentialsProvider({
name: 'MoralisAuth',
credentials: {
message: {
label: 'Message',
type: 'text',
placeholder: '0x0',
},
signature: {
label: 'Signature',
type: 'text',
placeholder: '0x0',
},
},
async authorize(credentials) {
try {
const { message, signature } = credentials;
await Moralis.start({ apiKey: process.env.MORALIS_API_KEY });
const { address, profileId, expirationTime } = (await Moralis.Auth.verify({ message, signature, network: 'evm' })).raw;
const user = { address, profileId, expirationTime, signature };
return user;
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
return null;
}
},
}),
],
callbacks: {
async jwt({ token, user }) {
user && (token.user = user);
return token;
},
async session({ session, token }) {
session.expires = token.user.expirationTime;
session.user = token.user;
return session;
},
},
session: {
strategy: 'jwt',
},
});
/api/auth/request-message.js
import Moralis from 'moralis';
const config = {
domain: process.env.APP_DOMAIN,
statement: 'Please sign this message to confirm your identity.',
uri: process.env.NEXTAUTH_URL,
timeout: 60,
};
export default async function handler(req, res) {
const { address, chain, network } = req.body;
await Moralis.start({ apiKey: process.env.MORALIS_API_KEY });
try {
const message = await Moralis.Auth.requestMessage({
address,
chain,
network,
...config,
});
res.status(200).json(message);
} catch (error) {
res.status(400).json({ error });
console.error(error);
}
}