Commands
Wallet Auth (Sign in with Ethereum)
Wallet Auth is our native support for Sign in With Ethereum.
Creating the nonce
Since the user can modify the client, it's important to create the nonce in the backend. The nonce must be at least 8 alphanumeric characters in length.
app/api/nonce.ts
import {cookies} from "next/headers"; import {(NextRequest, NextResponse)} from "next/server";
export function GET(req: NextRequest) {
// Expects only alphanumeric characters
const nonce = crypto.randomUUID().replace(/-/g, "");
// The nonce should be stored somewhere that is not tamperable by the client
// Optionally you can HMAC the nonce with a secret key stored in your environment
cookies().set("siwe", nonce, { secure: true });
return NextResponse.json({ nonce });
}
Using the command
Sending & handling the command response
Below is the expected input for walletAuth
.
interface WalletAuthInput {
nonce: string
expirationTime?: Date
statement?: string
requestId?: string
notBefore?: Date
}
Using the async walletAuth
command.
app/page.tsx
import { MiniKit, WalletAuthInput } from '@worldcoin/minikit-js'
// ...
const signInWithWallet = async () => {
if (!MiniKit.isInstalled()) {
return
}
const res = await fetch(`/api/nonce`)
const { nonce } = await res.json()
const {commandPayload: generateMessageResult, finalPayload} = await MiniKit.commandsAsync.walletAuth({
nonce: nonce,
requestId: '0', // Optional
expirationTime: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000),
notBefore: new Date(new Date().getTime() - 24 * 60 * 60 * 1000),
statement: 'This is my statement and here is a link https://worldcoin.com/apps',
})
// ...
The returned message (in final payload) will include a signature compliant with ERC-191.
You're welcome to use any third party libraries to verify the payloads for SIWE, but since World App uses Safe addresses that do not implement isValidSignature
we created a helper function for you.
Under the hood, we use the wallet's EOA private key to sign the message but the address is the Safe address.
type MiniAppWalletAuthSuccessPayload = {
status: 'success'
message: string
signature: string
address: string
version: number
}
app/page.tsx
const signInWithWallet = async () => {
if (!MiniKit.isInstalled()) {
return
}
const res = await fetch(`/api/nonce`)
const { nonce } = await res.json()
const { commandPayload: generateMessageResult, finalPayload } = await MiniKit.commandsAsync.walletAuth({
nonce: nonce,
requestId: '0', // Optional
expirationTime: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000),
notBefore: new Date(new Date().getTime() - 24 * 60 * 60 * 1000),
statement: 'This is my statement and here is a link https://worldcoin.com/apps',
})
if (finalPayload.status === 'error') {
return
} else {
const response = await fetch('/api/complete-siwe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
payload: finalPayload,
nonce,
}),
})
}
}
You can now additionally access the user's wallet address from the minikit object.
const walletAddress = MiniKit.walletAddress
// or
const walletAddress = window.MiniKit?.walletAddress
Verifying the Login
Finally, complete the sign in by verifying the response from World App in your backend. Here we check the nonce matches the one we created earlier, and then verify the signature.
app/api/complete-siwe.ts
import { cookies } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
import { MiniAppWalletAuthSuccessPayload, verifySiweMessage } from '@worldcoin/minikit-js'
interface IRequestPayload {
payload: MiniAppWalletAuthSuccessPayload
nonce: string
}
export const POST = async (req: NextRequest) => {
const { payload, nonce } = (await req.json()) as IRequestPayload
if (nonce != cookies().get('siwe')?.value) {
return NextResponse.json({
status: 'error',
isValid: false,
message: 'Invalid nonce',
})
}
try {
const validMessage = await verifySiweMessage(payload, nonce)
return NextResponse.json({
status: 'success',
isValid: validMessage.isValid,
})
} catch (error: any) {
// Handle errors in validation or processing
return NextResponse.json({
status: 'error',
isValid: false,
message: error.message,
})
}
}