Skip to content

Commit

Permalink
feat: added how to authenticate signed message (#2415)
Browse files Browse the repository at this point in the history
* feat: added how to authenticate signed message

* fix: forgotten import

---------

Co-authored-by: Guillermo Alejandro Gallardo Diez <[email protected]>
  • Loading branch information
gagdiez and Guillermo Alejandro Gallardo Diez authored Jan 7, 2025
1 parent 3b26286 commit 617e69d
Show file tree
Hide file tree
Showing 2 changed files with 21 additions and 83 deletions.
88 changes: 5 additions & 83 deletions docs/2.build/4.web3-apps/backend/backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
id: backend-login
title: Authenticate NEAR Users
---

import {Github} from "@site/src/components/codetabs"

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.
Expand Down Expand Up @@ -60,86 +63,5 @@ const signature = wallet.signMessage({ message, recipient, nonce: challenge, cal
### 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")

class Payload {
constructor({ message, nonce, recipient, callbackUrl }) {
// The tag's value is a hardcoded value as per
// defined in the NEP [NEP413](https://github.com/near/NEPs/blob/master/neps/nep-0413.md)
this.tag = 2147484061;
this.message = message;
this.nonce = nonce;
this.recipient = recipient;
if (callbackUrl) { this.callbackUrl = callbackUrl }
}
}

const payloadSchema = {
struct: {
tag: "u32",
message: "string",
nonce: { array: { type: "u8", len: 32 } },
recipient: "string",
// Must be of type { option: "string" }
callbackUrl: { option: "string" },
},
};

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 };
```
<Github fname="authenticate.js" language="javascript"
url="https://github.com/near-examples/near-api-examples/blob/main/javascript/examples/verify-signature/authentication.js" />
16 changes: 16 additions & 0 deletions docs/4.tools/near-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,22 @@ When deleting an access key, you need to specify the public key of the key you w

---

## Validate Message Signatures

Users can sign messages using the `wallet-selector` `signMessage` method, which returns a signature. This signature can be verified using the following code:

<Tabs groupId="api">
<TabItem value="js" label="🌐 JavaScript">

<Github fname="authenticate.js" language="javascript"
url="https://github.com/near-examples/near-api-examples/blob/main/javascript/examples/verify-signature/authentication.js" />

</TabItem>
</Tabs>

---


## Utilities

### NEAR to yoctoNEAR {#near-to-yoctonear}
Expand Down

0 comments on commit 617e69d

Please sign in to comment.