Skip to main content
NEAR’s NEP-413 standard enables users to authenticate into backend services by signing messages with their wallet. This provides a secure, decentralized authentication method without requiring passwords or centralized identity providers.
This authentication method is based on NEP-413, NEAR’s standard for message signing and verification.

How It Works

Backend authentication with NEAR wallets follows a three-step process:
1

Create a Challenge

The backend generates a cryptographically secure random challenge (nonce) that the user must sign.
2

Request Signature

The frontend asks the user to sign the challenge with their wallet, which proves ownership of the account.
3

Verify Signature

The backend verifies that the signature is valid and corresponds to the claimed user account.
Backend authentication flow diagram

Backend: Create a Challenge

The first step is to create a unique challenge for the user to sign. This should be a cryptographically secure random value.
import { randomBytes } from 'crypto';

// API endpoint to request a challenge
app.post('/auth/challenge', async (req, res) => {
  const { accountId } = req.body;
  
  // Generate a 32-byte random challenge
  const challenge = randomBytes(32);
  
  // Store challenge temporarily (with expiration)
  // Use Redis, in-memory cache, or database
  await redis.setex(
    `auth:challenge:${accountId}`,
    300, // 5 minutes expiration
    challenge.toString('base64')
  );
  
  res.json({
    challenge: challenge.toString('base64'),
    message: 'Sign in to My App',
    recipient: 'my-app.near', // Your app identifier
  });
});
Always use cryptographically secure random number generators like crypto.randomBytes(). Never use Math.random() or similar functions for security-critical operations.

Frontend: Request Signature

Once you have the challenge from your backend, request the user to sign it with their wallet.
import { useNearWallet } from 'near-connect-hooks';
import { useState } from 'react';

function LoginButton() {
  const { signNEP413Message, accountId } = useNearWallet();
  const [loading, setLoading] = useState(false);

  const handleLogin = async () => {
    if (!accountId) {
      alert('Please connect your wallet first');
      return;
    }

    setLoading(true);

    try {
      // 1. Request challenge from backend
      const challengeResponse = await fetch('https://api.myapp.com/auth/challenge', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ accountId }),
      });

      const { challenge, message, recipient } = await challengeResponse.json();

      // 2. Request user to sign the challenge
      const signature = await signNEP413Message({
        message,
        recipient,
        nonce: Buffer.from(challenge, 'base64'),
      });

      // 3. Send signature to backend for verification
      const authResponse = await fetch('https://api.myapp.com/auth/verify', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          accountId,
          signature: signature.signature,
          publicKey: signature.publicKey,
        }),
      });

      const { token } = await authResponse.json();

      // 4. Store authentication token
      localStorage.setItem('authToken', token);
      
      alert('Successfully authenticated!');
    } catch (error) {
      console.error('Authentication failed:', error);
      alert('Authentication failed. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <button onClick={handleLogin} disabled={loading}>
      {loading ? 'Authenticating...' : 'Sign In with NEAR'}
    </button>
  );
}
The message signing feature (NEP-413) is supported by most NEAR wallets including Meteor Wallet, Here Wallet, MyNearWallet, Near Snap, Nightly Wallet, WELLDONE Wallet, Sender, and Intear Wallet.

Backend: Verify Signature

The final step is to verify that the signature is valid and corresponds to the user’s account.
import { utils } from 'near-api-js';
import bs58 from 'bs58';

app.post('/auth/verify', async (req, res) => {
  const { accountId, signature, publicKey } = req.body;

  try {
    // 1. Retrieve the stored challenge
    const storedChallenge = await redis.get(`auth:challenge:${accountId}`);
    
    if (!storedChallenge) {
      return res.status(400).json({ error: 'Challenge expired or not found' });
    }

    // 2. Reconstruct the message that was signed
    const message = 'Sign in to My App';
    const recipient = 'my-app.near';
    const nonce = Buffer.from(storedChallenge, 'base64');

    // NEP-413 message format
    const payload = {
      tag: 2147484061, // NEP-413 tag
      message,
      recipient,
      nonce: Array.from(nonce),
    };

    const borshPayload = borsh.serialize(
      {
        struct: {
          tag: 'u32',
          message: 'string',
          recipient: 'string',
          nonce: { array: { type: 'u8', len: 32 } },
        },
      },
      payload
    );

    // 3. Verify the signature
    const publicKeyObj = utils.PublicKey.from(publicKey);
    const signatureBytes = bs58.decode(signature);
    
    const isValid = publicKeyObj.verify(
      new Uint8Array(borshPayload),
      signatureBytes
    );

    if (!isValid) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // 4. Verify the public key belongs to the account
    const provider = new providers.JsonRpcProvider({ 
      url: 'https://rpc.mainnet.near.org' 
    });
    
    const accessKeys = await provider.query({
      request_type: 'view_access_key_list',
      account_id: accountId,
      finality: 'final',
    });

    const hasKey = accessKeys.keys.some(
      (key) => key.public_key === publicKey
    );

    if (!hasKey) {
      return res.status(401).json({ error: 'Public key not found on account' });
    }

    // 5. Delete the used challenge
    await redis.del(`auth:challenge:${accountId}`);

    // 6. Generate JWT token
    const token = jwt.sign(
      { accountId, publicKey },
      process.env.JWT_SECRET,
      { expiresIn: '7d' }
    );

    res.json({ token, accountId });
  } catch (error) {
    console.error('Verification error:', error);
    res.status(500).json({ error: 'Verification failed' });
  }
});
You can also use the near-api-js verify-signature example as a reference implementation.

Complete Example

Here’s a complete end-to-end example combining frontend and backend:
app/components/Auth.tsx
import { useNearWallet } from 'near-connect-hooks';
import { useState, useEffect } from 'react';

const API_URL = 'https://api.myapp.com';

export function Auth() {
  const { signNEP413Message, accountId, signOut } = useNearWallet();
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // Check if user has valid token
    const token = localStorage.getItem('authToken');
    if (token) {
      // Verify token is still valid
      fetch(`${API_URL}/auth/verify-token`, {
        headers: { Authorization: `Bearer ${token}` },
      })
        .then((res) => res.ok && setIsAuthenticated(true))
        .catch(() => localStorage.removeItem('authToken'));
    }
  }, []);

  const handleLogin = async () => {
    if (!accountId) return;

    setLoading(true);
    try {
      // Step 1: Get challenge
      const challengeRes = await fetch(`${API_URL}/auth/challenge`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ accountId }),
      });
      const { challenge, message, recipient } = await challengeRes.json();

      // Step 2: Sign challenge
      const signature = await signNEP413Message({
        message,
        recipient,
        nonce: Buffer.from(challenge, 'base64'),
      });

      // Step 3: Verify signature
      const verifyRes = await fetch(`${API_URL}/auth/verify`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          accountId,
          signature: signature.signature,
          publicKey: signature.publicKey,
        }),
      });

      const { token } = await verifyRes.json();
      localStorage.setItem('authToken', token);
      setIsAuthenticated(true);
    } catch (error) {
      console.error('Login failed:', error);
      alert('Authentication failed');
    } finally {
      setLoading(false);
    }
  };

  const handleLogout = () => {
    localStorage.removeItem('authToken');
    setIsAuthenticated(false);
    signOut();
  };

  if (!accountId) {
    return <p>Please connect your wallet first</p>;
  }

  if (isAuthenticated) {
    return (
      <div>
        <p>Authenticated as {accountId}</p>
        <button onClick={handleLogout}>Logout</button>
      </div>
    );
  }

  return (
    <button onClick={handleLogin} disabled={loading}>
      {loading ? 'Authenticating...' : 'Authenticate with NEAR'}
    </button>
  );
}

Security Considerations

Always set a short expiration time (5-10 minutes) for challenges to prevent replay attacks. Store challenges with TTL in Redis or a similar cache.
Each challenge should only be used once. Delete the challenge immediately after successful verification.
Always verify that the public key used to sign the message actually belongs to the claimed account by querying the NEAR network.
All authentication requests must be sent over HTTPS to prevent man-in-the-middle attacks.
Implement rate limiting on authentication endpoints to prevent brute force attacks.

Next Steps

Wallet Login

Learn about frontend wallet integration

Integrate Contracts

Call smart contract methods from your app

NEP-413 Standard

Read the full NEP-413 specification

Example Code

View complete authentication examples

Build docs developers (and LLMs) love