Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added how to authenticate signed message #2415

Merged
merged 2 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading