Логин в один клик с Minter Link
Tags: minter, cryptography, extension, login
Published: 2020-09-25 14:02:18

Чем проще регистрация на вашем сайте  -  тем больше людей ей воспользуются. Людей отталкивает необходимость заполнять длинные формы с большим количеством полей. Аутентификация через соц. сети в один клик -  это то, к чему пришли все популярные сайты. 

Эта статья рассматривает метод аутентификации c использованием Minter блокчейна: безопасный способ войти на сайт в один клик при помощи расширения Minter Link.

![Minter Link Login Flow](https://dev-to-uploads.s3.amazonaws.com/i/ojbvbln229u2rszsdywh.gif)

## Как это работает

Основная идея состоит в том, что криптография позволяет нам проверить является ли человек владельцем аккаунта при помощи криптографической подписи. Зная только публичный ключ владельца, мы можем убедиться что сообщение подписано им при помощи приватного ключа. Рассмотрим процесс по шагам.

### Браузерное расширение Minter Link
**Minter Link** — это браузерное расширение, доступное для [Google Chrome](https://chrome.google.com/webstore/detail/minter-link/clnbjajcehaehmnkihhkcfdpgahdpghj). Основная функция расширения — мультикошелек для Minter блокчейна. После установки вы получаете возможность управлять кошельками, отправлять и получать монеты в сети Minter.

Но Minter Link, помимо этого, позволяет взаимодействовать со страницами, которые вы просматриваете в браузере. Разработчики сайтов могут получать мгновенные обновление от вашего кошелька (если вы дали такое разрешение) и делать запросы на оплату и цифровую подпись сообщений. Для этого существует библиотека Minter Connect.

### JavaScript библиотека Minter Connect
**Minter Connect** — это мостик между вашим приложением и браузерным расширением Minter Link. Библиотека, с одной стороны, позволяет получать данные о статусе расширения и данные о публичном адресе, с другой — позволяет запрашивать у пользователя разрешение на доступ к публичному адресу, криптографическую подпись и делать запросы на отправку транзакций.

Подробнее о том, как пользоваться библиотекой — на странице репозитория https://github.com/minterscan/minter_connect

### Minter Link Login Flow

![Minter Link Login Flow](https://dev-to-uploads.s3.amazonaws.com/i/6m42haojv5h8rlh7cby5.png)

#### Шаг нулевой: подготовить БД (back end)
Дополним модель пользователя сайта. У каждого пользователя добавим два поля: `publicAddress` и `nonce`. Учтем, что публичный адрес должен быть уникальным полем.

#### Шаг первый: сгенерировать `nonce` (back end)
Для каждого существующего пользователя сгенерируем `nonce`. Это должно быть случайное число или строка.

#### Шаг второй: получить `nonce` (front end)
Передадим на backend `publicAddress` и запросим свой `nonce`.

Для того, чтобы приложение имело доступ к публичному адресу, его необходимо сначала запросить (один раз). После подтверждения Minter Link добавит домен в список разрешенных и вы всегда сможете знать, что пользователь вернулся к вам на сайт (при условии, что Minter Link разблокирован). 

Порядок действия такой: 
* проверяем наличие расширения (установлено или нет)
* проверяем статус расширения (разблокировано или нет)
* проверяем доступ к публичному адресу
* если доступа нет — сначала запрашиваем его 
* если доступ есть — делаем запрос на backend, отправляя `publicAddress` и получаем назад `nonce`

#### Шаг третий: отправить запрос на подпись (front end)
Подписываем `nonce` своим приватным ключом. Для этого делаем вызов расширению и передаем ему `nonce`. Назад ожидаем получить подписанное сообщение (в HEX формате) и цифровую ECDSA подпись (также в HEX формате).

#### Шаг четвертый: проверить подпись (back end)
Проверяем подписанное сообщение при помощи публичного ключа и параметров подписи. Если сообщение действительно подписано владельцем публичного ключа — считаем что пользователь прошел аутентификацию. Формируем JWT и возвращаем его на frontend.

#### Шаг пятый: обновить `nonce` (back end)
Обновляем `nonce`. Чтобы предотвратить повторную аутентификацию с той же подписью (на случай, если она была скомпрометирована), мы сделаем так, что при следующей попытке входа на сайт пользователю понадобится подписать новое сообщение (новый `nonce`)

### Примеры кода

**HTML**

```html
<!-- Расширение не установлено -->
<p if="!isMinterLinkInstalled">Minter Link is not installed</p>

<!-- Расширение заблокировано -->
<p if="isMinterLinkInstalled && !isMinterLinkUnlocked">Minter Link is locked</p>

<div if="isMinterLinkInstalled && isMinterLinkUnlocked">
  <!-- Нет доступа к публичному адресу -->
  <button onclick="connect()" if="!minterPublicAddress">Connect</button>

  <!-- Есть доступ к публичному адресу -->
  <button onclick="login()" if="minterPublicAddress && !isAuthenticated">Login</button>

  <!-- Пользователь вошел на сайт -->
  <button onclick="logout()" if="isAuthenticated">Logout</button>
</div>
```

**JavaScript**

```javascript
import MinterConnect, { MinterLinkObservableProps } from 'minter-connect'

let isMinterLinkInstalled = false
let isMinterLinkUnlocked = false
let minterPublicAddress = ''
let isAuthenticated = false

// Устанавливаем соединение с расширением
const minterConnect = new MinterConnect('My awesome application')

// Подписываемся на события
minterConnect.subscribe(MinterLinkObservableProps.IsInstalled, (value) => {
  isMinterLinkInstalled = value
})

minterConnect.subscribe(MinterLinkObservableProps.IsUnlocked, (value) => {
  isMinterLinkUnlocked = value
})

minterConnect.subscribe(MinterLinkObservableProps.Wallet, (value) => {
  minterPublicAddress = value
})

// Запрашиваем у пользователя доступ к публичному адресу
function connect () {
  if (!isMinterLinkInstalled) return
  if (!isMinterLinkUnlocked) return

  minterConnect.connectRequest()
}

async function login () {
  if (!isMinterLinkInstalled) return
  if (!isMinterLinkUnlocked) return
  if (!minterPublicAddress) return

  try {
    // Запрашиваем nonce на backend
    const nonce = await api.getNonce(minterPublicAddress)
    // Запрашиваем у пользователя криптографическую подпись
    const { message, signature } = await minterConnect.signRequest(nonce)
    // Проверяем подпись на backend
    const jwt = await api.auth(minterPublicAddress, signature)

    isAuthenticated = true
  } catch (e) {
    console.error(e)
  }
}

function logout () {
  isAuthenticated = false
}
```

**Node.JS**

```javascript
import { mPrefixToHex } from 'minterjs-util'
import { pubToAddress } from 'ethereumjs-util/dist/account'
import { addHexPrefix, toBuffer, bufferToHex } from 'ethereumjs-util/dist/bytes'
import { hashPersonalMessage, ecrecover, fromRpcSig } from 'ethereumjs-util/dist/signature'

function auth (address, signature) {
  const nonce = getNonce(publicKey)
  const isValifSignature = verifySignature(nonce, address, signature)

  if (!isValidSignature) throw new Error('Invalid signature')

  await updateNonce(minterPublicAddress)

  // Возвращаем JWT
  return jwt.sign(...)
}

function verifySignature (nonce, address, signature) {
  // Воссоздаем подписанное сообщение
  const msgBuffer = toBuffer(addHexPrefix(Buffer.from(nonce).toString('hex')))
  // Хешируем сообщение
  const msgHash = hashPersonalMessage(msgBuffer)
  // Получаем параметры ECDSA подписи
  const sigParams = fromRpcSig(signature)
  // Восстанавливаем из сообщения публичный ключ, используя параметры подписи, полученные от клиента
  const recoveredPublicKey = ecrecover(msgHash, sigParams.v, sigParams.r, sigParams.s)
  // Конвертируем публичный ключ в Mx адрес
  const recoveredAddress = pubToAddress(recoveredPublicKey)
  // Сравниваем полученный адрес и адрес, полученный от клиента
  const isValidSignature = mPrefixToHex(publicKey) === bufferToHex(recoveredAddress)

  return isValidSignature
}

async function getNonce (publicKey) {
  const user = await db.getUserByPublicKey(publicKey)

  return user.nonce
}

async function updateNonce (publicKey) {
  const user = await db.getUserByPublicKey(publicKey)

  user.nonce = Math.floor(Math.random() * 1000000)

  return user.save()
}
```
После того, как мы получили JWT, дальше работаем с ним как обычно. Ко всем приватным запросам к API добавляем в заголовок токен и проверяем его валидность на сервере. Кроме еще одного способа аутентификации в структуре приложения ничего больше не меняется. 

### Плюсы и минусы

Это просто, удобно и безопасно. Криптография позволяет удостовериться нам, что только владелец приватного ключа может подписать сообщение и мы легко можем это проверить, без прямого доступа к блокчейну, только на основе математики.

* **Повышенная безопасность**. Приватный ключ кошелька хранится в расширении пользователя локально в зашифрованном виде. Атаковать напрямую локальное хранилище очень сложно, у каждого расширения свое изолированное окружение и даже в случае утечки зашифрованной строки с приватным ключом потребуется **очень много ресурсов**, чтобы расшифровать строку, закодированную AES c 256-битным ключом.

* **Упрощенный UX**. Вместо того, чтобы вводить логин и пароль, пользователю нужно нажать всего одну кнопку (окей, две, в случае если это его первый визит на сайт). Не нужно ничего запоминать, все "ключи" лежат у него прямо в браузере. 

* **Конфиденциальность**. Все хранится локально, никакие данные не передаются третьим лицам.

Ничто не мешает вам использовать Minter Link как дополнительный способ аутентификации, в дополнение к классическому входу через почту и пароль и/или социальные сети. 

От минусов никуда не деться, серебряной пули не существует. 

* **От пользователя требуется установить Minter Link**. Если аудитория вашего сайта не интересуется криптовалютами и блокчейн-технологиями — скорее всего о Minter они не слышали и вам это не нужно. 

* **Потребуется потратить некоторое время на доработку кода**. Упрощенный пример выглядит просто, но дьявол кроется в деталях. Если у вас уже есть работающий сложный сайт, интеграция нового способа аутентификации может потребовать усилий и времени.

* **Это не работает на мобильных устройствах**. Популярные мобильные браузеры не поддерживают установку расширений. Есть несколько экспериментальных браузеров, которые это умеют, но ими пользуются далеко не все, так что об этом лучше пока забыть. 

Спасибо за внимание.

* [Minter Link](https://github.com/minterscan/minter_link)
* [Minter Connect](https://www.npmjs.com/package/minter-connect)
* [Minter Link Playground](https://github.com/minterscan/minter_link_playground) | [Demo](https://minterscan.github.io/minter_link_playground/)

Читайте также: [Оплата в один клик при помощи Minter Link](https://dev.to/whitesunset/minter-link-491p).