Автентифікація Flutter додатку з NEAR Protocol

Покроковий туторіал імплементації автентифікації з Layer-1 блокчейном NEAR Protocol використовуючи Flutter і Golang

--

NEAR x Flutter

Після цього туторіалу ви зможете реалізувати автентифікацію за допомогою NEAR Protocol з перевіркою ключа на бекенді і перенаправленням користувача на будь-який NEAR гаманець з потрібними вам параметрами для найкращого UX/UI.

Authentication Demo

Технології

У цьому туторіалі будуть використовуватись Flutter, Golang, базовий JavaScript та HTML.

Flutter — це фреймворк з відкритим кодом для створення графічних додатків на різні платформи включаючи iOS, Android, web та інші. Flutter буде використовуватись для створення сторінки автентифікації.

Golang — високо-рівнева мова програмування створена Google. Частіше всього використовується для бекенду, створення CLI утиліт, тощо. Golang буде використаний для перевірки згенерованого публічного ключа та генерації токену для користувача.

JavaScript та HTML — основні інструменти для веб розробки. JavaScript та HTML потрібні, щоб перенаправити користувача з вашого веб-сайту на веб-сайт гаманця з передачею усіх потрібних параметрів.

HTML файл для перенаправлення на NEAR гаманець

HTML файл для перенаправлення на NEAR гаманець потрібен для того, щоб у синьому полі на сторінці гаманця відображався домен вашого веб-сайту.

Коли автентифікація відбувається з веб-сайту, то браузер підставляє Referer заголовок в HTTP запиті, в якому міститься інформація про домен з якого було здійснено перенаправлення на NEAR гаманець. Наприклад, якщо потрібно автентифікувати користувача з веб-сайту https://coaty.world, то при перенаправленні буде виставлений заголовок Referer: https://coaty.world/, який потім буде відображений, як coaty.world на сторінці гаманця.

Більше інформації про Referer заголовок можна дізнатись за посиланням

Заголовок Referer виставляється автоматично браузером при переході з однієї сторінки на іншу, а у випадку з Flutter браузер відкривається відразу з веб-сайтом NEAR гаманця. Оскільки Referer буде порожнім, то у синьому полі з назвою додатку буде Unknown App.

Для того, щоб забезпечити користувача найкращим UX, потрібно щоб в синьому полі відображалась назва вашого додатку.

Приклад сторінки NEAR гаманця з Referer заголовком і без нього:

NEAR wallet application name example

У вікні з автентифікацією на Flutter буде відкриватись HTML файл, який буде автоматично перенаправляти користувача на NEAR гаманець. Таким чином, не помітно для користувача буде виставлений заголовок Referer з доменом вашого веб-сайту.

Повний відкритий код доступний за посиланням

const walletUrl = 'https://wallet.testnet.near.org/';
const contractId = 'auth.coatyworld1.testnet';
const urlParams = new URLSearchParams(window.location.search);
const publicKey = urlParams.get('public_key');
const successUrl = urlParams.get('success_url') || 'client://success';
const failureUrl = urlParams.get('failure_url') || 'client://failure';

window.location.href = `${walletUrl}/login/?contract_id=${contractId}&success_url=${successUrl}&failure_url=${failureUrl}&public_key=${publicKey}`;

walletUrl — це константа, яка містить в собі посилання на гаманець, який ви хочете використовувати для підключення до смарт контракту. Наприклад, https://wallet.near.org/, https://meteorwallet.app/, https://www.mynearwallet.com/.

contractId — це константа, яка містить в собі ідентифікатор контракту, до якого буде підключений користувач.

urlParams — це константа, яка містить в собі параметри з URL.

publicKey — це константа, яка передається користувачем з клієнту. Константа буде містити в собі згенерований публічний ключ, який буде доданий на аккаунт користувача. Це обовʼязковий параметр, який повинен бути переданий з клієнту.

successUrl, failureUrl — це посилання на які буде виконане перенаправлення з гаманця у випадку успішного або невдалого додавання. Користувач буде перенаправлений на successUrl у першому випадку і відповідно на failureUrl у другому.

window.location.ref — це змінна, яка містить в собі теперішній URL сторінки. При присвоєнні нового значення window.location.ref буде здійснено перенаправлення на новий URL з виставленням Referer заголовку на домен сторінки з якої відбувається перенаправлення.

Отже, цей HTML файл буде використаний, як посередник між вашим клієнтом і гаманцем NEAR щоб правильно виставити Referer заголовок.

Приклади використання: https://your-domain.com/index.html?public_key=ed25519:xyz&success_url=client://success&failure_url=client://failure
https://your-domain.com/index.html?public_key=ed25519:xyz2

Flutter клієнт

На клієнті буде відбуватись генерація приватного та публічного ключів для підключення до потрібного вам смарт контракту.

Детальніше про роботу ключів на NEAR Protocol можна дізнатись у статті за посиланням.

Після підключення до смарт контракту, потрібно відправити згенерований публічний ключ і підпис на бекенд для перевірки і генерації автентифікаційного токену.

Повний відкритий код доступний за посиланням

Для клієнту потрібно чотири додаткових бібліотеки:

https://pub.dev/packages/flutter_web_auth - для відкриття зручного вікна автентифікації без відкриття браузеру на iOS.

https://pub.dev/packages/bs58 - для base58 енкодингу і декодингу.

https://pub.dev/packages/ed25519_edwards - для генерації ed25519 публічного і приватного ключів та підпису приватним ключем.

https://pub.dev/packages/http - для виконання http викликів на бекенд.

Кожна бібліотека містить у собі туторіал по встановленню

У цьому туторіалі будуть розібрані тільки дві основні функції для автентифікації, оскільки все інше — це віджети, які до автентифікації не мають ніякого відношення і можуть бути легко замінені іншими віджетами.

Перша функція _nearLogin буде відкривати вікно автентифікації для NEAR гаманця та здійснювати перенаправлення до клієнту з інформацією про аккаунт після підключення NEAR аккаунту.

Спершу, потрібно згенерувати пару приватного і публічного ключів.

final keyPair = ed.generateKey();

Енкодувати байти публічного ключа в base58 формат.

final generatedPublicKey = 'ed25519:${base58.encode(keyPair.publicKey.bytes as Uint8List)}';

Згенерувати URL, для переходу в гаманець. В параметрах URL обовʼязково повинен бути вказаний public_key з попереднього кроку. success_url та failure_url не обовʼязкові, оскільки в HTML файлі виставлені значення за замовчуванням.

final uri = Uri.http(_backendHost, _nearLoginPath, {
'public_key': generatedPublicKey,
'success_url': _successURL,
'failure_url': _failureURL,
});

Використовуючу бібліотеку flutter_web_auth, відкриваємо вікно автентифікації.

final result = await FlutterWebAuth.authenticate(url: uri.toString(), callbackUrlScheme: 'client');

Якщо результат не починається з _successURL, тобто автентифікація завершилась невдало, то викидаємо помилку про невдалу автентифікацію.

if (!result.toString().startsWith(_successURL)) {
throw Exception('Authentication failed');
}

Дістаємо ідентифікатор аккаунту і публічний ключ з URL результату.

final queryParameters = Uri.parse(result).queryParameters;
final accountID = queryParameters['account_id'];
final publicKey = queryParameters['public_key'];

Підписуємо повідомлення у вигляді ідентифікатору аккаунту за допомогою приватного ключа. На бекенді буде можливість перевірити за допомогою публічного ключа чи було це повідомлення підписане за допомогою приватного ключа.

final signature = base64Encode(ed.sign(keyPair.privateKey, utf8.encode(accountID!) as Uint8List));

Повертаємо всі згенеровані дані з функції, щоб передати їх на бекенд.

return NEARData(accountID: accountID, publicKey: publicKey!, signature: signature);

Друга функція складається з двох рядків: запит до серверу з даними, які були отримані з попередньої функції і отримання результату з згенерованим автентифікаційним токеном.

Future<AuthResponse> _backendLogin(NEARData data) async {
final result = await http.post(Uri.http(_backendHost, _backendAuthPath), body: jsonEncode(data.toJson()));
return AuthResponse.fromJson(jsonDecode(result.body));
}

Бекенд повертає json з одним полем token, яке виступає автентифікаційним токеном у цьому прикладі.

Отже, клієнт відкриває автентифікаційне браузерне вікно з сторінкою веб-сайту, яка перенаправляє користувача на гаманець NEAR. Після закінчення автентифікації браузерне вікно закривається, повертаючи ідентифікатор аккаунту та публічний ключ або нічого у випадку невдалої автентифікації. Після отримання даних від NEAR Protocol, створюється підпис і відправляється на бекенд для верифікації. Після успішної верифікації відбувається генерація автентифікаційного токену для клієнту.

Golang бекенд

Бекенд потрібен, щоб перевірити підпис і публічний ключ, які приходять від клієнту та у випадку успішної перевірки, згенерувати токен для клієнту.

Також бекенд буде повертати HTML файл для перенаправлення користувача на потрібний вам гаманець NEAR, але цей HTML файл може знаходитись на будь-якому сервері, він потрібен лише для Referer заголовку з доменом вашого додатку.

Повний відкритий код доступний за посиланням

Для бекенду потрібні два додаткових модулі:

https://github.com/btcsuite/btcd/tree/master/btcutil — для base58 енкодингу і декодингу.

https://github.com/ybbus/jsonrpc — для виконання RPC запитів до NEAR Protocol.

Кожен модуль містить у собі туторіал по встановленню

Бекенд складається з двох основних функцій, які будуть перевіряти дані, які надходять з клієнту. У випадку успішної перевірки, буде повернений згенерований автентифікаційний токен, в іншому випадку буде повернена помилка.

Перша функція VerifySignature, яка перевіряє за допомогою публічного ключа чи було повідомлення підписане приватним ключем з згенерованої пари ключів на клієнті.

Спершу, потрібно декодувати підпис з base64 до слайсу байтів.

func VerifySignature(accountID, publicKey, signature string) error {
decodedSignature, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return err
}

Потім декодувати публічний ключ з base58 до слайсу байтів, обрізуючи префікс ed25519:.

decodedPublicKey := base58.Decode(publicKey[8:])
if err != nil {
return err
}

За допомогою функції Verify верифікувати підпис, використовуючи повідомлення і публічний ключ.

if ok := ed25519.Verify(decodedPublicKey, []byte(accountID), decodedSignature); !ok {
return errors.New("invalid signature")
}

Помилка на будь-якому з цих етапів означає, що підпис не є правильним або були передані не вірні дані.

Наступним кроком буде перевірка наявності публічного ключа на аккаунті. Для цього потрібно зробити RPC запит view_access_key на NEAR Protocol.

RPC запит відбувається у функції ViewAccessKey.

Спочатку потрібно створити запит з аргументами, які складаються з типу реквесту: view_access_key, з завершеності блокчейну: final, тобто фінальний стан блокчейну, ідентифікатору користувача: accountID, згенерованого публічного ключа користувача: publicKey.

request := ViewAccessKeyRequest{
RequestType: requestType,
Finality: finality,
AccountID: accountID,
PublicKey: publicKey,
}

Блок з транзакцією добавляння публічнюго ключа стане фінальним після підтвердження двох наступних блоків. На даний момент, середній час обробки одного блоку займає біля однієї секунди.

Час обробки одного блоку можна побачити за посиланням

Оскільки запит до бекенду відбувається моментально після добавлення публічного ключа до аккаунту, на момент виконання, блок з транзакцією може бути не фінальним, тому потрібно зачекати 3 секунди перед RPC запитом.

time.Sleep(3 * time.Second)

Потім виконується RPC запит до NEAR Protocol, який повинен повернути інформацію про надані доступи для смарт контракту або помилку у випадку, якщо ключ не був доданий до аккаунту.

var response ViewAccessKeyResponse
if err := c.rpcClient.CallFor(ctx, &response, method, &request); err != nil {
return ViewAccessKeyResponse{}, err
}
if response.Error != "" {
return ViewAccessKeyResponse{}, errors.New(response.Error)
}
return response, nil

Після отримання відповіді від NEAR Protocol, потрібно перевірити чи був наданий доступ до смарт контракту саме вашого додатку.

if viewAccessKeyResponse.Permission.FunctionCall.ReceiverID != contractID {
http.Error(w, "invalid contract id", http.StatusUnauthorized)
return
}

Після успішного проходження усіх кроків, можна створювати автентифікаційний токен для користувача і поєднувати всі дані з ідентифікатором аккаунту оскільки він є унікальним на блокчейні NEAR Protocol.

Висновок

Автентифікація через NEAR Protocol на Flutter не є складною, але потребує декілька обережних кроків зі створенням і перевіркою ключів та перенаправленням користувача з веб-сайту на NEAR гаманець для забезпечення найкращого UX.

Near Ukraine Guild 🇺🇦 це швидкозростаюча спільнота з України, що націлена на надання високоякісного освітнього контенту та допомоги для розвитку сильної спільноти розробників/підприємців/ентузіастів в екосистемі Near Protocol

⚡️ Отримати 10% річних в токенах NEAR

--

--