Membangun Web Chat XMPP yang Aman dengan Java: Jangan Simpan Password XMPP di JavaScript
Pada implementasi web chat XMPP sederhana, browser biasanya langsung terhubung ke server menggunakan JavaScript.
Contohnya:
connection.connect(jid, password, onConnect);
Kode tersebut memang dapat bekerja. Namun, ada pertanyaan penting:
Apakah password XMPP aman jika diproses di JavaScript?
Password pengguna tidak otomatis dapat dicuri oleh pengguna lain hanya karena ada JavaScript di browser. Namun, password tersebut tetap berada di sisi client saat proses login. Jika aplikasi memiliki celah XSS, ekstensi browser berbahaya, perangkat pengguna terinfeksi, atau password disimpan secara keliru di browser, risiko keamanan akan meningkat.
Untuk aplikasi internal, enterprise, atau sistem yang menangani data penting, desain yang lebih baik adalah: browser tidak pernah mengetahui password XMPP.
Arsitektur yang Lebih Aman
Alih-alih browser terhubung langsung ke XMPP, gunakan backend Java sebagai gateway.
Browser
→ HTTPS + Session Login
→ Backend Spring Boot
→ TLS XMPP
→ XMPP Server
Dalam pola ini:
- Pengguna login ke aplikasi web.
- Browser menerima session cookie aplikasi.
- Cookie tidak dapat dibaca JavaScript.
- Browser berkomunikasi ke backend melalui WebSocket.
- Backend Java mengirim atau menerima pesan dari server XMPP.
- Password XMPP hanya tersedia pada server backend.
Dengan demikian, JavaScript tidak lagi memiliki akses terhadap password XMPP.
Perbandingan Dua Pendekatan
| Aspek | Browser Langsung ke XMPP | Backend Java Gateway |
|---|---|---|
| Password XMPP | Diproses pada browser | Hanya tersedia di backend |
| Koneksi XMPP | Browser ke XMPP server | Backend ke XMPP server |
| Login pengguna | Akun XMPP langsung | Login aplikasi atau SSO |
| Kontrol akses | Lebih bergantung pada XMPP server | Dapat dikontrol di backend dan XMPP |
| Audit log | Terbatas | Lebih mudah dicatat |
| Integrasi bisnis | Lebih sulit | Lebih mudah |
| Cocok untuk | Prototype sederhana | Aplikasi internal dan enterprise |
Mengapa Password XMPP Tidak Boleh Ditulis di JavaScript?
Kesalahan yang paling berbahaya adalah menyimpan credential langsung dalam source code.
Contoh yang tidak aman:
const jid = "gateway@chat.example.com";
const password = "password-rahasia";
connection.connect(jid, password, onConnect);
Siapa pun yang membuka source JavaScript di browser dapat melihat credential tersebut.
Hal yang juga perlu dihindari:
localStorage.setItem("xmppPassword", password);
localStorage dapat dibaca oleh JavaScript. Jika ada celah XSS, script berbahaya dapat membaca data tersebut.
Jangan menyimpan password, API key, token layanan, atau credential server di:
- File JavaScript
- File HTML
- Local storage
- Session storage
- Repository Git
- Console log
- Error message
- File konfigurasi yang ikut di-deploy ke frontend
Konsep Secure XMPP Gateway
Pada arsitektur gateway, backend memiliki akun XMPP khusus, misalnya:
secure-gateway@chat.nirantara.id
Akun tersebut hanya digunakan oleh aplikasi backend untuk berbicara ke XMPP server.
Credential akun gateway disimpan sebagai environment variable di server:
XMPP_HOST=xmpp-internal.example.com
XMPP_DOMAIN=chat.example.com
XMPP_SERVICE_USER=secure-gateway
XMPP_SERVICE_PASSWORD=password-xmpp-yang-kuat
Backend membaca credential tersebut saat aplikasi berjalan.
String host = System.getenv("XMPP_HOST");
String domain = System.getenv("XMPP_DOMAIN");
String username = System.getenv("XMPP_SERVICE_USER");
String password = System.getenv("XMPP_SERVICE_PASSWORD");
Nilai tersebut tidak dikirim ke browser.
Untuk production, penggunaan secret manager lebih baik daripada memasukkan secret langsung ke file konfigurasi. Contohnya dapat menggunakan Docker Secrets, Kubernetes Secrets, Vault, atau credential manager pada server.
Login Pengguna Menggunakan Spring Security
Pengguna sebaiknya login ke aplikasi, bukan login langsung menggunakan password XMPP.
Spring Security dapat digunakan untuk mengelola autentikasi, session, logout, dan pembatasan halaman.
Contoh konfigurasi sederhana:
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/error").permitAll()
.anyRequest().authenticated())
.formLogin(form -> form.defaultSuccessUrl("/", true))
.logout(logout -> logout.logoutSuccessUrl("/login?logout"))
.build();
}
Setelah pengguna berhasil login, Spring Security membuat session aplikasi.
Browser menerima cookie session, tetapi cookie tersebut harus dikonfigurasi dengan aman.
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.same-site=strict
Arti konfigurasi tersebut:
| Konfigurasi | Fungsi |
|---|---|
http-only=true | JavaScript tidak dapat membaca cookie session |
secure=true | Cookie hanya dikirim melalui HTTPS |
same-site=strict | Membantu mengurangi risiko CSRF lintas situs |
Dengan pola ini, frontend tidak perlu menyimpan token atau password XMPP.
WebSocket antara Browser dan Backend
Browser tetap membutuhkan komunikasi real-time untuk chat. Namun, WebSocket yang digunakan adalah WebSocket milik aplikasi backend, bukan koneksi langsung ke server XMPP.
Browser
→ wss://aplikasi.internal/ws/chat
→ Spring Boot WebSocket Handler
→ XMPP Gateway
Contoh JavaScript:
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const socket = new WebSocket(
`${protocol}//${location.host}/ws/chat`
);
Karena WebSocket berada pada domain aplikasi yang sama, browser secara otomatis mengirim session cookie yang sudah dimiliki pengguna.
JavaScript tidak perlu membaca cookie tersebut.
socket.onmessage = ({ data }) => {
const response = JSON.parse(data);
if (response.type === 'sent') {
console.log('Pesan berhasil dikirim.');
}
};
Mengirim Pesan dari Browser ke Backend
Frontend cukup mengirim data yang diperlukan, misalnya JID penerima dan isi pesan.
socket.send(JSON.stringify({
to: 'operator@chat.example.com',
body: 'Mohon periksa tiket prioritas tinggi.'
}));
Data tersebut diterima oleh WebSocket handler di Spring Boot.
@Override
protected void handleTextMessage(
WebSocketSession session,
TextMessage message
) throws Exception {
JsonNode request = objectMapper.readTree(
message.getPayload()
);
String recipient = request.path("to").asText().trim();
String body = request.path("body").asText().trim();
String sender = session.getPrincipal().getName();
xmppGateway.send(recipient, body, sender);
}
Perhatikan bagian ini:
String sender = session.getPrincipal().getName();
Backend dapat mengetahui siapa pengguna yang mengirim pesan berdasarkan session login aplikasi, bukan berdasarkan nilai yang dikirim oleh browser.
Jangan mempercayai informasi sender jika nilainya dikirim langsung dari JavaScript, karena pengguna dapat mengubah request melalui browser.
Validasi Pesan dan Penerima
Backend tidak boleh menerima semua input tanpa validasi.
Contoh validasi sederhana untuk penerima:
if (!recipient.matches("^[^@\\s]+@chat\\.example\\.com$")) {
throw new IllegalArgumentException(
"Recipient is not allowed"
);
}
Contoh pembatasan panjang pesan:
if (body.isBlank() || body.length() > 2000) {
throw new IllegalArgumentException(
"Message length must be 1-2000 characters"
);
}
Pada sistem nyata, validasi penerima tidak cukup hanya menggunakan regex domain. Sistem sebaiknya memeriksa:
- Apakah pengguna memiliki hak chat ke penerima tersebut?
- Apakah penerima terdaftar dalam organisasi?
- Apakah pengguna boleh masuk ke group tertentu?
- Apakah user sedang diblokir?
- Apakah ada batas jumlah pesan?
- Apakah pesan berisi data sensitif?
Aturan tersebut dapat dikelola menggunakan database, role, atau RBAC.
Menghubungkan Backend Java ke XMPP Server
Backend Java dapat menggunakan library Smack untuk terhubung ke XMPP server.
XMPPTCPConnectionConfiguration config =
XMPPTCPConnectionConfiguration.builder()
.setHost(host)
.setPort(5222)
.setXmppDomain(
JidCreate.domainBareFrom(domain)
)
.setSecurityMode(
ConnectionConfiguration.SecurityMode.required
)
.build();
connection = new XMPPTCPConnection(config);
connection.connect();
connection.login(username, password);
Bagian berikut penting:
.setSecurityMode(
ConnectionConfiguration.SecurityMode.required
)
Konfigurasi tersebut meminta koneksi XMPP menggunakan TLS. Jangan menggunakan koneksi XMPP tanpa enkripsi untuk aplikasi production.
Setelah backend berhasil login, pesan dapat dikirim menggunakan ChatManager.
Chat chat = ChatManager.getInstanceFor(connection)
.chatWith(JidCreate.entityBareFrom(recipient));
chat.send(body);
Satu Akun Gateway atau Akun XMPP per Pengguna?
Ada dua pola yang dapat dipilih.
1. Satu Akun XMPP Gateway
Backend menggunakan satu akun service:
secure-gateway@chat.nirantara.id
Keunggulannya:
- Credential lebih mudah dikelola
- Browser tidak menyimpan password XMPP
- Lebih mudah membuat audit log
- Cocok untuk bot, notifikasi, dan sistem internal
Kekurangannya:
- Penerima akan melihat pesan berasal dari akun gateway
- Identitas pengguna aplikasi perlu dimasukkan sebagai metadata
Contoh:
[demo] Mohon periksa tiket prioritas tinggi.
2. Akun XMPP per Pengguna
Setiap pengguna memiliki akun XMPP masing-masing, tetapi autentikasi tetap dilakukan secara aman melalui backend atau identity provider.
Pola ini cocok jika sistem membutuhkan:
- Presence per pengguna
- Pesan benar-benar muncul dari akun pengguna
- Group chat berbasis identitas individual
- Riwayat dan device management XMPP
Untuk pola ini, gunakan mekanisme autentikasi modern seperti SASL OAuth/OIDC jika didukung oleh server XMPP. Alternatif lain adalah menyimpan credential pengguna di secret vault pada server, tetapi desain tersebut harus dilakukan dengan sangat hati-hati.
Pengamanan Tambahan untuk Production
Selain memindahkan password XMPP ke backend, beberapa pengamanan berikut perlu diterapkan.
Gunakan HTTPS dan WSS
Akses aplikasi menggunakan:
https://chat-aplikasi.example.com
Dan WebSocket:
wss://chat-aplikasi.example.com/ws/chat
Hindari http:// dan ws:// di production.
Terapkan Rate Limiting
Batasi jumlah pesan yang dapat dikirim pengguna dalam periode tertentu.
Contohnya:
Maksimal 30 pesan per menit per pengguna
Rate limiting membantu mengurangi spam, abuse, dan serangan otomatis.
Buat Audit Log
Catat aktivitas penting seperti:
- User login
- User logout
- Koneksi WebSocket
- Pesan dikirim
- Penerima pesan
- Waktu kejadian
- Error koneksi XMPP
- Penolakan akses
Untuk data sensitif, jangan menyimpan isi pesan penuh tanpa kebutuhan bisnis yang jelas.
Hindari Pesan Error Terlalu Detail
Jangan mengirim detail error server ke browser.
Kurang aman:
XMPP authentication failed for secure-gateway because password is invalid.
Lebih aman:
Pesan tidak dapat dikirim. Silakan coba kembali.
Detail teknis tetap dapat disimpan pada log backend yang aman.
Gunakan Reconnect dengan Backoff
Koneksi backend ke XMPP dapat terputus karena jaringan atau restart server.
Backend perlu memiliki mekanisme reconnect, tetapi jangan melakukan percobaan koneksi secara terus-menerus tanpa jeda.
Gunakan pendekatan exponential backoff:
1 detik
→ 2 detik
→ 4 detik
→ 8 detik
→ maksimal 60 detik
Penutup
Menghubungkan browser langsung ke XMPP memang cepat untuk membuat prototype. Namun, untuk aplikasi yang lebih aman, password XMPP sebaiknya tidak pernah diproses atau disimpan di JavaScript.
Gunakan backend Java sebagai secure gateway:
Browser → Session Aplikasi → Backend Java → XMPP Server
Dengan desain ini, browser hanya berinteraksi dengan aplikasi melalui HTTPS dan WebSocket. Password XMPP, aturan akses, audit log, validasi pesan, serta integrasi sistem dikelola di backend.
Pendekatan ini membuat aplikasi chat lebih siap untuk digunakan pada lingkungan internal, proses bisnis, notifikasi operasional, bot, maupun integrasi AI assistant.
Published by Nirantara
Nirantara is an Indonesian Enterprise AI company specializing in Enterprise AI, RAG, Intelligent Search, and AI Automation. Article written by jsturana.