near-sign-verify
Create and validate NEP-413 signed messages for API authentication

npm install near-sign-verify
[!IMPORTANT]
It is highly recommended that you implement state and nonce validation, initiated by a handshake with your backend. This crucial step helps mitigate CSRF attacks and replay attacks. The Full Backend Integration example below demonstrates this secure flow.
Cookbook
1. Simple Client-Side Signing with a Wallet
This example shows how to use fastintear (or any wallet object that implements signMessage as per NEP-413) to sign a message client-side, attach the token to an Authorization header, and verify it with default settings.
This simple strategy is fine for a non-production application and provides a basic timestamp-based nonce validation.
import * as wallet from "fastintear";
import { sign, verify } from 'near-sign-verify';
const authToken = await sign("login attempt", {
signer: wallet,
recipient: 'your-service.near',
});
fetch('https://api.example.com/endpoint', {
headers: { 'Authorization': `Bearer ${authToken}` },
});
try {
const result = await verify(authToken, {
expectedRecipient: "your-service.near",
nonceMaxAge: 300000,
});
console.log('Successfully verified for account:', result.accountId);
console.log('Message from token:', result.message);
} catch (error: any) {
console.error('Token verification failed:', error.message);
}
2. Signing with a KeyPair
This flow demonstrates signing a message using a KeyPair directly. This is useful for testing, backend-initiated signing (if you manage keys securely, such as when building a wallet), or simulated environments.
Important: NEP-413 standard explicitly states that messages MUST be signed using a Full Access Key for security. While near-sign-verify can verify signatures from Function Call Access Keys by setting requireFullAccessKey: false, this is NOT recommended for production authentication flows without significant additional validation on your end.
import { sign } from "near-sign-verify";
import { KeyPair } from '@near-js/crypto';
const fullAccessKeyPair = KeyPair.fromRandom('ed25519');
const accountId = "you.near";
const authToken = await sign("login attempt", {
signer: fullAccessKeyPair.toString(),
accountId: accountId,
recipient: "your-service.near",
});
fetch('https://api.example.com/endpoint', {
headers: { 'Authorization': `Bearer ${authToken}` },
});
try {
const result = await verify(authToken, {
expectedRecipient: "your-service.near",
nonceMaxAge: 300000,
});
console.log('Successfully verified for account:', result.accountId);
console.log('Message from token:', result.message);
} catch (error: any) {
console.error('Token verification failed:', error.message);
}
3. Full Backend Integration (Recommended for Production)
This strategy leverages your backend to manage nonces and states, providing the highest level of security against replay and CSRF attacks.
Flow:
- Client Request: Frontend requests authentication parameters (message, nonce, state) from the backend.
- Backend Generates: Backend generates a unique nonce and state, stores them, and sends them to the frontend.
- Client Signs: Frontend uses these parameters to sign the message with the NEAR wallet.
- Client Sends Signed Token: Frontend sends the
authToken back to the backend.
- Backend Verifies: Backend verifies the
authToken, strictly validating the nonce and state against its stored values.
import { sign } from "near-sign-verify";
import { toHex, fromHex } from "@fastnear/utils";
onClick("Login with NEAR Button", async () => {
const response = await fetch("https://your-service.com/api/auth/initiate-login");
const { state, message, nonce, recipient } = await response.json();
const authToken = await sign(message, {
signer: wallet,
recipient: recipient,
nonce: fromHex(nonce),
state: state,
});
const verifyResponse = await fetch("https://your-service.com/api/auth/verify-login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ authToken }),
});
if (verifyResponse.ok) {
console.log("Authentication successful!");
} else {
console.error("Authentication failed:", await verifyResponse.json());
}
});
const authRequests = new Map();
const usedNonces = new Set<string>();
POST("https://your-service.com/api/auth/initiate-login", (req, res) => {
const state = randomBytes(32).toString("hex");
const nonce = randomBytes(32).toString("hex");
const message = "Authorize my app";
const recipient = "your-service.com";
authRequests.set(state, {
nonce: nonce,
message: message,
recipient: recipient,
timestamp: Date.now()
});
res.json({
state: state,
message: message,
nonce: nonce,
recipient: recipient
});
});
POST("https://your-service.com/api/auth/verify-request", async (req, res) => {
const { authToken } = req.body;
try {
const parsedData = parseAuthToken(authToken);
const {
nonce: receivedNonce,
state: receivedState,
message: receivedMessage,
recipient: receivedRecipient
} = parsedData;
const storedAuthRequest = authRequests.get(receivedState);
const result = await verify(authToken, {
expectedState: storedAuthRequest.state
validateNonce: (nonceFromToken: Uint8Array): boolean => {
const receivedNonceHex = toHex(nonceFromToken);
if (receivedNonceHex !== storedAuthRequest.nonce) {
console.error("Nonce mismatch: Received nonce does not match expected nonce for the state.");
return false;
}
if (usedNonces.has(receivedNonceHex)) {
console.error("Nonce already used (replay attack detected).");
return false;
}
usedNonces.add(receivedNonceHex);
return true;
},
expectedMessage: storedAuthRequest.message,
validateRecipient: (recipientFromToken: string): boolean => {
const ALLOWED_LIST = ["your-service.com", "app.your-service.com", "your-service.near"];
return ALLOWED_LIST.includes(recipientFromToken);
}
});
authRequests.delete(receivedState);
res.json({ success: true, accountId: result.accountId, message: result.message });
} catch (e: any) {
console.error("Token verification failed:", e.message);
if (receivedState && authRequests.has(receivedState)) {
authRequests.delete(receivedState);
}
res.status(400).json({ success: false, error: e.message });
}
});
Debugging
You can use the parseAuthToken helper method to inspect the outcome of sign.
import { parseAuthToken, type NearAuthData } from "near-sign-verify";
const authHeader = c.req.header('Authorization');
const authToken = authHeader.substring(7);
const authData: NearAuthData = parseAuthToken(authToken);
console.log("authData", authData);