--- id: backend-login title: Authenticate NEAR Users --- Recently NEAR has approved a new standard that, among other things, enables users to authenticate into a backend service. The basic idea is that the user will sign a challenge with their NEAR wallet, and the backend will verify the signature. If the signature is valid, then the user is authenticated. --- ## Backend Auth with a NEAR Wallet Authenticating users is a common use-case for backends and web applications. This enables services to provide a personalized experience to users, and to protect sensitive data. To authenticate a user, the backend must verify that the user is who they say they are. To do so, the backend must verify that the user has access to a full-access key that is associated with their account. For this three basic steps are needed: 1. Create a challenge for the user to sign. 2. Ask the user to sign the challenge with the wallet. 3. Verify the signature corresponds to the user. ### 1. Create a Challenge Assume we want to login the user into our application named `application-name`. We first need to create a challenge that the user will sign with their wallet. For this, it is recommended to use a cryptographically secure random number generator to create the challenge. ```js import { randomBytes } from 'crypto' const challenge = randomBytes(32) const message = 'Login with NEAR' ``` :::note Here we use [crypto.randomBytes](https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback) to generate a 32 byte random buffer. ::: ### 2. Ask the User to Sign the Challenge The `signMessage` method needed to sign the challenge is supported by these wallets: - Meteor Wallet - Here Wallet - Near Snap - Nightly Wallet - WELLDONE Wallet - NearMobileWallet - MyNearWallet - Sender The message that the user needs to sign contains 4 fields: - Message: The message that the user is signing. - Recipient: The recipient of the message. - Nonce: The challenge that the user is signing. - Callback URL: The URL that the wallet will call with the signature. ```js // Assuming you setup a wallet selector so far const signature = wallet.signMessage({ message, recipient, nonce: challenge, callbackUrl: }) ``` ### 3. Verify the Signature Once the user has signed the challenge, the wallet will call the `callbackUrl` with the signature. The backend can then verify the signature. ```js const naj = require('near-api-js') const js_sha256 = require("js-sha256") export async function authenticate({ accountId, publicKey, signature }) { // A user is correctly authenticated if: // - The key used to sign belongs to the user and is a Full Access Key // - The object signed contains the right message and domain const full_key_of_user = await verifyFullKeyBelongsToUser({ accountId, publicKey }) const valid_signature = verifySignature({ publicKey, signature }) return valid_signature && full_key_of_user } export function verifySignature({ publicKey, signature }) { // Reconstruct the payload that was **actually signed** const payload = new Payload({ message: MESSAGE, nonce: CHALLENGE, recipient: APP, callbackUrl: cURL }); const borsh_payload = borsh.serialize(payloadSchema, payload); const to_sign = Uint8Array.from(js_sha256.sha256.array(borsh_payload)) // Reconstruct the signature from the parameter given in the URL let real_signature = Buffer.from(signature, 'base64') // Use the public Key to verify that the private-counterpart signed the message const myPK = naj.utils.PublicKey.from(publicKey) return myPK.verify(to_sign, real_signature) } export async function verifyFullKeyBelongsToUser({ publicKey, accountId }) { // Call the public RPC asking for all the users' keys let data = await fetch_all_user_keys({ accountId }) // if there are no keys, then the user could not sign it! if (!data || !data.result || !data.result.keys) return false // check all the keys to see if we find the used_key there for (const k in data.result.keys) { if (data.result.keys[k].public_key === publicKey) { // Ensure the key is full access, meaning the user had to sign // the transaction through the wallet return data.result.keys[k].access_key.permission == "FullAccess" } } return false // didn't find it } // Aux method async function fetch_all_user_keys({ accountId }) { const keys = await fetch( "https://rpc.testnet.near.org", { method: 'post', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: `{"jsonrpc":"2.0", "method":"query", "params":["access_key/${accountId}", ""], "id":1}` }).then(data => data.json()).then(result => result) return keys } module.exports = { authenticate, verifyFullKeyBelongsToUser, verifySignature }; ```