Panduan Implementasi Autentikasi JWT & Nostr NIP-98
Autentikasi merupakan hal yang paling krusial dalam pengembangan aplikasi seperti web agar data sensitif dari pengguna terdaftar dapat terjaga keamanannya. Di masa kini, authentication1 sudah banyak caranya, mulai dari OAuth, Username/Email + Password tradisional, kombinasi dengan JWT, dan sebagainya.
Dalam artikel ini, kita akan mempelajari cara membangun sistem autentikasi yang lebih aman dengan Nostr NIP-982. Meskipun Nostr dikenal sebagai protokol jejaring sosial yang terdesentralisasi, dalam panduan ini kita akan mengadaptasi mekanisme NIP-98 untuk sistem yang bersifat tersentralisasi (seperti aplikasi web tradisional). Menariknya, pendekatan ini tetap fleksibel; Anda bisa menggunakannya di sistem terpusat saat ini, namun tetap siap jika ingin beralih ke ekosistem yang sepenuhnya terdesentralisasi di lain waktu nanti.
Daftar Isi Tutorial
Agar pembahasannya terstruktur dan tidak membingungkan, saya akan membagi tutorial ini menjadi beberapa poin penting yang bisa Anda ikuti:
- Prasyarat & Hal yang Dibutuhkan
- Sekilas Tentang Nostr dan Event serta Proses Penandatanganannya
- Kunci dan Identitas Nostr
- Flow Implementasi NIP-98 - HTTP Auth
- Implementasi Backend (Node.js + Express)
- Implementasi Frontend
- Kelebihan & Kekurangan Penggunaan Nostr NIP-98
- Troubleshooting & FAQ
1. Prasyarat & Hal yang Dibutuhkan
Sebelum kita masuk ke bagian teknis, pastikan Anda sudah menyiapkan beberapa hal berikut agar proses belajarnya lancar:
- Pemahaman dasar Node.js dan Express.js.
- Library
nostr-tools3 (untuk penanganan kriptografi). - Extension Nostr seperti Alby atau Nos2x (Sangat disarankan).
- Akun Nostr atau pasangan kunci (Public/Private Key).
2. Sekilas Tentang Nostr dan Event serta Proses Penandatanganannya
Saat ini ada protokol baru bernama Nostr, sebuah protokol jejaring sosial yang bersifat terdesentralisasi (Tonton Penjelasan Nostr). Meskipun lahir di ekosistem desentralisasi, mekanisme keamanannya sangat fleksibel untuk diadopsi ke berbagai jenis arsitektur. Salah satu fitur yang sangat berguna adalah NIP-98 (HTTP Auth).
Dengan mekanisme ini, proses autentikasi dilakukan dengan cara menandatangani event khusus secara kriptografi dan mengirimnya melalui Authorization header. Event yang telah ditandatangani kemudian diverifikasi di sisi server. Dengan alur ini, kunci atau kata sandi tidak pernah dikirimkan ke internet, melainkan tetap tersimpan aman di perangkat pengguna.
Nostr secara teknis berkomunikasi melalui WebSocket dengan saling mengirim paket event. Namun, kita dapat mengadaptasi alur NIP-98 ini ke dalam sistem tersentralisasi (menggunakan protokol HTTP tradisional). Ini memberikan keuntungan unik: sistem Anda bisa tetap tersentralisasi secara infrastruktur, namun memiliki keamanan identitas sekelas sistem terdesentralisasi.
Apa itu Event di Nostr?
Nostr adalah paket data terstuktur yang berisi pesan dan metadata yang kemudian ditandatangani dengan kunci privat pengguna. Event itu seperti “record” atau “objek pesan” yang menyatakan: “Aku (pemilik pubkey ini) mengatakan / melakukan X pada waktu Y”, dan dibuktikan dengan tanda tangan kriptografi. Untuk menjaga keteraturan, maka dibuatlah Nostr Improvement Proposal (NIP) untuk berbagai kebutuhan seperti autentikasi, komentar, membuat postingan, dan masih banyak lainnya. Lihat Nostr NIPs
Contoh struktur dasar event Nostr (NIP-01), sebuah event memiliki field utama seperti ini:
{
"id": "…", // hash SHA-256 dari event yg diserialisasi
"pubkey": "…", // kunci publik penulis (identitas Nostr)
"created_at": 0, // timestamp (detik sejak UNIX epoch)
"kind": 1, // tipe event (1 = note, 0 = metadata, dll)
"tags": [ // array tag (array of array string)
["e", "<event-id-lain>", "..."],
["p", "<pubkey-lain>", "..."]
],
"content": "…", // isi pesan / data utama
"sig": "…" // tanda tangan Schnorr atas id
}Penjelasan singkatnya:
- pubkey: identitas penulis event (x-only secp256k1, BIP-3404).
- created_at: kapan event dibuat.
- kind: jenis event, misalnya:
0: metadata (profil)1: note (status/post)7: reaction27235: HTTP Auth (NIP-98) seperti yang akan kita gunakan sekarang.
- tags: metadata tambahan dalam bentuk list, misalnya mention user (
["p", "<pubkey>"]), reply ke event lain (["e", "<id>"]), URL, dsb. Untuk NIP-98 dipakai["u", "<url>"],["method", "GET"], dll. - content: isi utama event, bisa teks, JSON (kadang diserialisasi string), dsb.
- id: hash SHA-256 dari bentuk ter-serialize
[0, pubkey, created_at, kind, tags, content]. - sig: tanda tangan Schnorr (BIP-340) atas
iddengan privkey penulis.
Agar paket event tersebut dapat terbukti valid, maka dilakukanlah proses pendandatanganan event yang dibuktikan melalui properti sig sebelum event tersebut dapat dikirimkan ke server. Meskipun kind/jenis event berbeda-beda pada tiap NIP, proses pendandatanganan event Nostr selalu menggunakan algoritma yang sama:
- Kurva eliptik secp256k1
- Algoritma signature Schnorr (Standar BIP-340)
- Hash: SHA-256
3. Kunci dan Identitas Nostr
Nostr menggunakan kunci publik dan private untuk identitas pengguna. Kunci publik adalah identitas pengguna yang akan digunakan sebagai identitas publik, sementara kunci private adalah kunci yang akan digunakan untuk menandatangani event.
WarningJangan pernah membagikan kunci private Anda, karena akan merusak keamanan data Anda. Sekali ketahuan, kunci private Anda akan terbuka dan dapat diakses oleh siapa saja.
- Private Key: Berupa 32 byte random, biasanya ditulis dalam bentuk hex (atau dimulai dengan
nsecjika menggunakan bech32). Kunci ini yang akan digunakan untuk menandatangani event. - Public Key: Berupa 32 byte hasil dari
private key * G, biasanya ditulis dalam bentuk hex (atau dimulai dengannpubjika menggunakan bech32).
Note
Gadalah generator point dari secp256k1.
Penandatanganan Event (Konseptual)
Secara konseptual, flow-nya adalah:
- Buat event (seperti contoh event NIP-01 di atas) tanpa
iddansig. - Hitung
iddari event tersebut dengan menggunakan algoritma SHA-256 (Event diserialisasi ke array JSON lalu di stringifikasi terlebih dahulu). - Hitung
sigdari event tersebut dengan menggunakan algoritma Schnorr BIP-340 dan private key.sig = sign(private_key, id) - Tambahkan
idandsigke event tersebut.
Setelah event ditandatangani, maka event tersebut dapat dikirimkan ke server Nostr. Jika dalam kasus kita, event tersebut akan dikirimkan melalui HTTP Auth (Authorization Header).
Verifikasi Event (Di Server)
Di server, verifikasi event tersebut dilakukan dengan cara:
- Rekonstruksi array yang ter-serialize dari event tersebut.
- Hitung
iddari event tersebut dengan menggunakan algoritma SHA-256. - Cek apakah
idyang dihitung sama denganidyang ada di event tersebut. - Lakukan verifikasi signature dengan menggunakan algoritma Schnorr BIP-340 dan public key.
schnorr_verify_bip340(public_key, sig, id) - Kemudian lakukan verifikasi lanjutan sesuai dengan spesifikasi NIP masing-masing. Dalam kasus kita, NIP-98.
Jika semua lolos verifikasi, maka event tersebut dapat dianggap valid.
4. Flow Implementasi NIP-98 - HTTP Auth
NIP-98 adalah sebuah event sementara yang digunakan untuk mengautentikasi pengguna di server Nostr. Event ini akan dikirimkan melalui HTTP Auth (Authorization Header) dengan format Nostr <base64(event)>.
Spesifikasi dan Cara Verifikasi
Spesifikasi Event:
- kind harus sama dengan
27235 - properti
contentharus kosong - properti
tagsharus berisi:u: URL yang akan diakses (Misalkan url login)method: HTTP method yang akan digunakan (Biasanya kita menggunakan POST)
iddidapatkan dari hasil hash SHA-256 dari array eventsigdidapatkan dari hasil signature Schnorr BIP-340 dari private key dan id
Contoh event NIP-98 yang sudah ditandatangani:
{
"kind": 27235,
"tags": [
["u", "https://example.com/login/nostr"],
["method", "POST"]
],
"created_at": 1633072800,
"id": "2ce42dd097abd880aab3cae0c2889791bd44a24ab2f5056781802285243cd39f",
"sig": "09df22a761c28b130e2e84068de6bd4d10affed5a786ebebae0c2a087241a878063b9f64ab58028eec07623ee47578c1d8969bff871fb2b3164ff18501887680"
}Client akan mengirimkan event tersebut melalui HTTP Auth (Authorization Header) dengan format Nostr <base64(event)> ke server. Lalu server akan melakukan verifikasi ketat:
- Kind harus sama dengan
27235. created_atharus dalam batas waktu kecil (reasonable time window). Misalnya 60 detik. Ini digunakan untuk memastikan bahwa event tersebut tidak terlalu lama.utag harus persis sama dengan absolute URL yang dikirimkan client.methodtag harus sama dengan HTTP method yang digunakan saat request.
Alur Kerja Implementasi
Secara lengkap, berikut adalah gambaran flow dari implementasi NIP-98 - HTTP Auth dalam bentuk sequence diagram:
Penjelasan singkatnya:
- Pertama-tama frontend user akan membuat event NIP-98 dengan kind=27235.
- Lalu frontend user akan mengirimkan event tersebut melalui HTTP Auth (Authorization Header) dengan format
Nostr <base64(event)>ke server. - Server akan melakukan verifikasi event tersebut secara ketat.
- Jika event tersebut valid, maka server akan membuat dan memberikan response berupa JWT (JSON Web Token).
- Server akan menyimpan JWT tersebut dalam cookie browser user.
- User dapat menggunakan JWT tersebut untuk mengakses API yang dilindungi (memerlukan autentikasi).
5. Implementasi Backend (Node.js + Express)
Sekarang kita masuk ke bagian selanjutnya. Di sini saya akan menggunakan Node.js dan Express.js untuk mendemonstrasikan bagaimana sisi backend bekerja. Kita akan buat satu service khusus untuk verifikasi dan satu endpoint untuk login.
Tahap 1 — Membuat Nostr Verification Service
Tujuan: Membuat fungsi reusable untuk memverifikasi validitas event NIP-98.
// services/nostrAuthService.ts
import { verifySignature, getEventHash } from 'nostr-tools';
import crypto from 'crypto';
export async function verifyNip98Event(event: any, options: { url: string, method: string, body?: any }) {
// 1. Verifikasi tipe event (kind 27235)
if (event.kind !== 27235) return false;
// 2. Cek window waktu (toleransi 60 detik)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - event.created_at) > 60) return false;
// 3. Cocokkan URL dan Method
const uTag = event.tags.find((t: any[]) => t[0] === 'u')?.[1];
const methodTag = event.tags.find((t: any[]) => t[0] === 'method')?.[1];
if (uTag !== options.url || methodTag !== options.method) return false;
// 4. Verifikasi Payload (jika ada body)
if (options.body && Object.keys(options.body).length > 0) {
const payloadTag = event.tags.find((t: any[]) => t[0] === 'payload')?.[1];
const bodyHash = crypto.createHash('sha256').update(JSON.stringify(options.body)).digest('hex');
if (payloadTag !== bodyHash) return false;
}
// 5. Verifikasi Signature secara Kriptografi
const id = getEventHash(event);
if (id !== event.id) return false;
return verifySignature(event);
}Tahap 2 — Membuat Route Login
Tujuan: Menerima request dari client, memverifikasi event, dan menerbitkan token sesi.
// routes/auth.ts
import express from 'express';
import jwt from 'jsonwebtoken';
import Iron from '@hapi/iron';
import { verifyNip98Event } from '../services/nostrAuthService';
const router = express.Router();
router.post('/login/nostr', async (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Nostr ')) {
return res.status(401).json({ error: 'Auth header tidak ditemukan' });
}
try {
const base64Event = authHeader.replace('Nostr ', '');
const event = JSON.parse(Buffer.from(base64Event, 'base64').toString('utf-8'));
// Ambil URL lengkap untuk verifikasi tag 'u'
const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
const isValid = await verifyNip98Event(event, {
url: fullUrl,
method: 'POST',
body: req.body
});
if (!isValid) return res.status(401).json({ error: 'Nostr event tidak valid' });
// Step 4 & 5: Issue JWT & Seal Session
const payload = { pubkey: event.pubkey };
const accessToken = jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '15m' });
const refreshToken = jwt.sign(payload, process.env.REFRESH_SECRET!, { expiresIn: '7d' });
// Menggunakan Iron untuk enkripsi cookie tingkat tinggi
const sealed = await Iron.seal({ accessToken, refreshToken }, process.env.IRON_PASSWORD!, Iron.defaults);
res.cookie('auth_session', sealed, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
return res.json({ success: true, user: event.pubkey });
} catch (err) {
return res.status(500).json({ error: 'Internal Server Error' });
}
});6. Implementasi Frontend
Nah, setelah bagian backend selesai, sekarang kita pindah ke sisi client (frontend). Di sini kita akan memanfaatkan interface window.nostr yang biasanya disediakan oleh extension seperti Alby agar kunci private user tetap aman.
Form Login Sederhana
<div id="login-form">
<h2>Login dengan Nostr</h2>
<p>Gunakan extension atau akun Anda dengan aman.</p>
<button id="btn-extension" onclick="loginWithExtension()">
Login via Extension (NIP-07)
</button>
<div style="margin: 20px 0;">Atau</div>
<input type="password" id="nsec-input" placeholder="Masukkan nsec Anda (Private Key)">
<button onclick="loginWithNsec()">Login via nsec</button>
</div>Logika Penandatanganan (Signing Request)
async function loginWithExtension() {
if (!window.nostr) return alert('Silahkan pasang nostr extension!');
const loginUrl = 'https://api.anda.com/login/nostr';
// 1. Buat Payload Event
const eventTemplate = {
kind: 27235,
created_at: Math.floor(Date.now() / 1000),
tags: [
['u', loginUrl],
['method', 'POST']
],
content: ''
};
// 2. Minta Extension untuk menandatangani
const signedEvent = await window.nostr.signEvent(eventTemplate);
// 3. Kirim via header Authorization
const base64Event = btoa(JSON.stringify(signedEvent));
const res = await fetch(loginUrl, {
method: 'POST',
headers: {
'Authorization': `Nostr ${base64Event}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ /* data tambahan */ })
});
if (res.ok) alert('Login Berhasil!');
}7. Kelebihan & Kekurangan Penggunaan Nostr NIP-98
Setiap teknologi memiliki pertimbangannya sendiri:
Kelebihan:
- Zero-Trust for Passwords: Server tidak pernah melihat private key atau password Anda.
- Interoperabilitas: Private key Anda bisa digunakan di berbagai platform Nostr tanpa perlu membuat ulang Private key yang baru.
- Keamanan Kriptografi: Menggunakan standar Schnorr Signature yang lebih modern dibanding ECDSA.
Kekurangan:
- Key Management: Jika user kehilangan private key, tidak ada fitur “Forgot Password”, namun karena kita hanya mengadaptasi NIP-98 ke dalam sistem yang tersentralisasi, maka user tetap bisa menggunakan private key yang baru jika kita sebagai pengembang mengizinkan pengguna untuk mengganti private key.
- Adopsi: Memerlukan edukasi tambahan bagi pengguna awam tentang konsep kunci publik/privat.
8. Troubleshooting & FAQ
Gejala: Error “NIP-98 event expired”
- Penyebab: Perbedaan waktu (clock skew) antara perangkat client dan server.
- Solusi: Pastikan server sinkron dengan NTP, dan berikan batas waktu yang lebih longgar jika diperlukan.
FAQ: Apakah aman menyimpan nsec di localStorage?
Sangat tidak disarankan dalam bentuk plain text. Jika terpaksa, enkripsilah nsec tersebut dengan password lain dan hanya simpan hasil enkripsinya.
Sekian artikel kali ini, semoga dapat membantu Anda dalam implementasi Nostr NIP-98 di aplikasi Anda. Dengan kombinasi ini, kita bisa memberikan standar keamanan yang jauh lebih tinggi bagi pengguna! 😊
Footnotes
-
JWT RFC 7519 - Official Document ↩
-
NIP-98 HTTP Auth - GitHub Spec ↩
-
nostr-tools Library - GitHub Repository ↩
-
BIP-340 Schnorr Signatures - Bitcoin Specification ↩