Use Wallet Authentication as the primary auth flow. Do not use the Verify command for login purposes.
Wallet Authentication provides native support for SIWE and it’s the recommended way to authenticate users in your mini app. This provides your app with access to a User object that contains the user’s wallet address, username, and other information.
export type User = {
  walletAddress?: string;
  username?: string;
  profilePictureUrl?: string;
  permissions?: {
    notifications: boolean;
    contacts: boolean;
  };
  optedIntoOptionalAnalytics?: boolean;
  worldAppVersion?: number;
  deviceOS?: string;
};
In addition we have two helper functions to make it easier to get User information.
MiniKit.getUserByAddress(address: string): Promise<User>
MiniKit.getUserByUsername(username: string): Promise<User>

// Returns
return {
    walletAddress: '0x...',
    username: 'John Doe',
    profilePictureUrl: 'https://example.com/profile.png',
};

How it works

Using NextAuth you can easily create and manage sessions for your app. The starter template is already set up with NextAuth. To extend this to other wallet providers, you simply need to trigger the wallet auth command and verify the response with verifySiweMessage.
Starting from World App 2.8.79 and higher, we support the standard SIWE verification library.

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.
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,
    });
  }
};

Success Result on World App

If implemented correctly, the user will see the following drawer on World App.

SIWE Implementations

Alternative Authentication Methods

  • OAuth (Google, Apple, etc.): These providers are supported but it’s recommended to trigger this outside of the World App and then redirect back to your mini app with the access token worldapp://mini-app?app_id=appId&path=/handle-oauth?accessToken=....
  • Sign in with World ID: Not recommended as it doesn’t provide the user’s wallet address.