Untuk proyek node.js baru yang sedang saya kerjakan, saya sedang berpikir untuk beralih dari pendekatan sesi berbasis cookie (maksud saya, menyimpan id ke toko nilai-kunci yang berisi sesi pengguna di browser pengguna) untuk pendekatan sesi berbasis token (tidak ada toko nilai kunci) menggunakan JSON Web Tokens (jwt).
Proyek ini adalah permainan yang memanfaatkan socket.io - memiliki sesi berbasis token akan berguna dalam skenario di mana akan ada beberapa saluran komunikasi dalam satu sesi (web dan socket.io)
Bagaimana cara menyediakan token / sesi tidak valid dari server menggunakan Pendekatan jwt?
Saya juga ingin memahami apa jebakan / serangan umum (atau tidak umum) yang harus saya perhatikan dengan paradigma semacam ini. Misalnya, jika paradigma ini rentan terhadap serangan yang sama / berbeda dengan pendekatan sesi store / cookie.
Jadi, katakan saya memiliki yang berikut (diadaptasi dari ini dan ini ):
Login Toko Sesi:
app.get('/login', function(request, response) {
var user = {username: request.body.username, password: request.body.password };
// Validate somehow
validate(user, function(isValid, profile) {
// Create session token
var token= createSessionToken();
// Add to a key-value database
KeyValueStore.add({token: {userid: profile.id, expiresInMinutes: 60}});
// The client should save this session token in a cookie
response.json({sessionToken: token});
});
}
Login Berbasis Token:
var jwt = require('jsonwebtoken');
app.get('/login', function(request, response) {
var user = {username: request.body.username, password: request.body.password };
// Validate somehow
validate(user, function(isValid, profile) {
var token = jwt.sign(profile, 'My Super Secret', {expiresInMinutes: 60});
response.json({token: token});
});
}
-
Logout (atau tidak valid) untuk pendekatan Session Store akan memerlukan pembaruan ke database KeyValueStore dengan token yang ditentukan.
Sepertinya mekanisme seperti itu tidak akan ada dalam pendekatan berbasis token karena token itu sendiri akan berisi info yang biasanya ada di toko kunci-nilai.
sumber
isRevoked
opsi ini, atau mencoba mereplikasi fungsi yang sama. github.com/auth0/express-jwt#revoked-tokensJawaban:
Saya juga telah meneliti pertanyaan ini, dan sementara tidak ada ide di bawah ini yang merupakan solusi lengkap, mereka mungkin membantu orang lain mengesampingkan ide, atau memberikan yang lebih lanjut.
1) Cukup hapus token dari klien
Jelas ini tidak melakukan apa-apa untuk keamanan sisi server, tetapi menghentikan penyerang dengan menghapus token dari keberadaan (mis. Mereka harus mencuri token sebelum logout).
2) Buat daftar hitam token
Anda dapat menyimpan token yang tidak valid hingga tanggal kedaluwarsa awal, dan membandingkannya dengan permintaan yang masuk. Ini tampaknya meniadakan alasan untuk token sepenuhnya berdasarkan di tempat pertama, karena Anda harus menyentuh database untuk setiap permintaan. Ukuran penyimpanan kemungkinan akan lebih rendah, karena Anda hanya perlu menyimpan token yang berada di antara waktu logout & waktu kedaluwarsa (ini adalah firasat, dan pasti tergantung pada konteks).
3) Simpan saja waktu kadaluwarsa singkat dan sering-seringlah memutarnya
Jika Anda menyimpan waktu kedaluwarsa token pada interval yang cukup singkat, dan meminta klien yang sedang berjalan melacak dan meminta pembaruan bila perlu, nomor 1 akan bekerja secara efektif sebagai sistem logout lengkap. Masalah dengan metode ini adalah bahwa hal itu membuat tidak mungkin untuk menjaga pengguna tetap masuk di antara penutupan kode klien (tergantung pada berapa lama Anda membuat interval kadaluwarsa).
Paket Kontinjensi
Jika pernah ada keadaan darurat, atau token pengguna dikompromikan, satu hal yang bisa Anda lakukan adalah memungkinkan pengguna untuk mengubah ID pencarian pengguna yang mendasarinya dengan kredensial login mereka. Ini akan membuat semua token terkait tidak valid, karena pengguna terkait tidak lagi dapat ditemukan.
Saya juga ingin mencatat bahwa itu adalah ide yang baik untuk memasukkan tanggal login terakhir dengan token, sehingga Anda dapat menegakkan relogin setelah beberapa periode waktu yang lama.
Dalam hal kesamaan / perbedaan sehubungan dengan serangan menggunakan token, posting ini membahas pertanyaan: https://github.com/dentarg/blog/blob/master/_posts/2014-01-07-angularjs-authentication-with-cookies -vs-token.markdown
sumber
2)
atas. Meskipun berfungsi dengan baik, secara pribadi saya tidak melihat banyak perbedaan dengan toko sesi tradisional. Saya kira persyaratan penyimpanan akan lebih rendah, tetapi Anda masih memerlukan database. Daya tarik terbesar JWT bagi saya adalah tidak menggunakan database sama sekali untuk sesi.Ide-ide yang diposting di atas adalah baik, tetapi cara yang sangat sederhana dan mudah untuk membatalkan semua JWT yang ada hanyalah mengubah rahasia.
Jika server Anda membuat JWT, menandatanganinya dengan rahasia (JWS) kemudian mengirimkannya ke klien, hanya dengan mengubah rahasia itu akan membatalkan semua token yang ada dan mengharuskan semua pengguna untuk mendapatkan token baru untuk diautentikasi karena token lama mereka tiba-tiba menjadi tidak valid sesuai ke server.
Tidak memerlukan modifikasi apa pun terhadap konten token yang sebenarnya (atau lookup ID).
Jelas ini hanya berfungsi untuk kasus darurat ketika Anda ingin semua token yang ada kedaluwarsa, untuk per token kedaluwarsa salah satu solusi di atas diperlukan (seperti waktu kedaluwarsa token pendek atau membatalkan kunci yang disimpan di dalam token).
sumber
Ini terutama komentar panjang yang mendukung dan membangun jawaban oleh @mattway
Diberikan:
Beberapa solusi lain yang diusulkan pada halaman ini menganjurkan memukul datastore pada setiap permintaan. Jika Anda menekan datastore utama untuk memvalidasi setiap permintaan otentikasi, maka saya melihat lebih sedikit alasan untuk menggunakan JWT daripada mekanisme otentikasi token yang ditetapkan lainnya. Anda pada dasarnya membuat JWT stateful, bukan stateless jika Anda pergi ke datastore setiap kali.
(Jika situs Anda menerima volume tinggi permintaan yang tidak sah, maka JWT akan menolaknya tanpa mengenai datastore, yang sangat membantu. Mungkin ada kasus penggunaan lain seperti itu.)
Diberikan:
Otentikasi JWT yang benar-benar tanpa kewarganegaraan tidak dapat dicapai untuk aplikasi web dunia nyata yang khas karena JWT tanpa kewarganegaraan tidak memiliki cara untuk menyediakan dukungan segera dan aman untuk kasus penggunaan penting berikut:
Akun pengguna dihapus / diblokir / ditangguhkan.
Kata sandi pengguna diubah.
Peran atau izin pengguna diubah.
Pengguna keluar oleh admin.
Data penting aplikasi apa pun dalam token JWT diubah oleh admin situs.
Anda tidak bisa menunggu token kadaluarsa dalam kasus ini. Pembatalan token harus segera terjadi. Selain itu, Anda tidak dapat mempercayai klien untuk tidak menyimpan dan menggunakan salinan token lama, baik dengan niat jahat atau tidak.
Oleh karena itu: Saya kira jawaban dari @ matt-way, # 2 TokenBlackList, akan menjadi cara paling efisien untuk menambahkan status yang diperlukan ke otentikasi berbasis JWT.
Anda memiliki daftar hitam yang menyimpan token-token ini hingga tanggal kedaluwarsa mereka tercapai. Daftar token akan sangat kecil dibandingkan dengan jumlah total pengguna, karena itu hanya harus menyimpan token daftar hitam sampai habis masa berlakunya. Saya akan menerapkan dengan meletakkan token yang tidak valid di redis, memcached atau datastore dalam memori lain yang mendukung pengaturan waktu kedaluwarsa pada kunci.
Anda masih harus melakukan panggilan ke db di-memori Anda untuk setiap permintaan otentikasi yang melewati autentikasi JWT awal, tetapi Anda tidak harus menyimpan kunci untuk seluruh set pengguna di sana. (Yang mungkin atau mungkin bukan masalah besar untuk situs tertentu.)
sumber
If the JWT contains the necessary data, the need to query the database for certain operations may be reduced, though this may not always be the case.
Saya akan mencatat nomor versi jwt pada model pengguna. Token jwt baru akan mengatur versinya untuk ini.
Ketika Anda memvalidasi jwt, cukup periksa bahwa ia memiliki nomor versi yang sama dengan pengguna versi jwt saat ini.
Setiap kali Anda ingin membatalkan jwts lama, cukup tanyakan nomor versi pengguna jwt.
sumber
Belum mencoba ini, dan ini menggunakan banyak informasi berdasarkan beberapa jawaban lainnya. Kompleksitas di sini adalah untuk menghindari panggilan penyimpanan data sisi server per permintaan informasi pengguna. Sebagian besar solusi lain memerlukan pencarian db per permintaan ke toko sesi pengguna. Itu baik-baik saja dalam skenario tertentu tetapi ini dibuat dalam upaya untuk menghindari panggilan seperti itu dan membuat apa pun yang disyaratkan sisi server menjadi sangat kecil. Anda akhirnya akan membuat ulang sesi sisi server, betapapun kecilnya untuk menyediakan semua fitur pembatalan paksa. Tetapi jika Anda ingin melakukannya di sini adalah intinya:
Tujuan:
Solusinya:
Ini mengharuskan Anda untuk mempertahankan daftar hitam (status) di server, dengan asumsi tabel pengguna berisi informasi pengguna yang dilarang. Daftar hitam sesi yang tidak valid - adalah daftar id pengguna. Daftar hitam ini hanya diperiksa selama permintaan token penyegaran. Entri harus tetap di dalamnya selama token token TTL. Setelah token penyegaran berakhir, pengguna akan diminta untuk masuk kembali.
Cons:
Pro:
Dengan solusi ini, penyimpanan data dalam memori seperti reddis tidak diperlukan, setidaknya tidak untuk informasi pengguna seperti Anda karena server hanya melakukan panggilan db setiap 15 menit atau lebih. Jika menggunakan reddis, menyimpan daftar sesi yang valid / tidak valid di sana akan menjadi solusi yang sangat cepat dan sederhana. Tidak perlu token penyegaran. Setiap token autentik akan memiliki id sesi dan id perangkat, mereka dapat disimpan dalam tabel reddis saat pembuatan dan dibatalkan jika diperlukan. Kemudian mereka akan diperiksa pada setiap permintaan dan ditolak saat tidak valid.
sumber
Pendekatan yang saya pertimbangkan adalah selalu memiliki nilai
iat
(dikeluarkan pada) di JWT. Kemudian ketika pengguna keluar, simpan stempel waktu itu pada catatan pengguna. Saat memvalidasi JWT, bandingkan saja denganiat
stempel waktu keluar terakhir. Jikaiat
lebih lama, maka itu tidak valid. Ya, Anda harus pergi ke DB, tapi saya akan selalu menarik catatan pengguna jika JWT valid.Kelemahan utama yang saya lihat adalah bahwa ini akan mengeluarkan mereka dari semua sesi mereka jika mereka berada di beberapa browser, atau memiliki klien seluler juga.
Ini juga bisa menjadi mekanisme yang bagus untuk membatalkan semua JWT dalam suatu sistem. Sebagian dari cek tersebut dapat bertentangan dengan stempel waktu global dari
iat
waktu valid terakhir .sumber
token_valid_after
, atau sesuatu. Luar biasa!Saya agak terlambat di sini, tapi saya pikir saya punya solusi yang layak.
Saya memiliki kolom "last_password_change" di database saya yang menyimpan tanggal dan waktu ketika kata sandi terakhir diubah. Saya juga menyimpan tanggal / waktu penerbitan di JWT. Ketika memvalidasi token, saya memeriksa apakah kata sandi telah diubah setelah token dikeluarkan dan apakah token itu ditolak meskipun belum kadaluarsa.
sumber
if (jwt.issue_date < user.last_pw_change) { /* not valid, redirect to login */}
Anda dapat memiliki bidang "last_key_used" pada DB Anda pada dokumen / catatan pengguna Anda.
Saat pengguna masuk dengan pengguna dan meneruskan, buat string acak baru, simpan di bidang last_key_used, dan tambahkan ke muatan ketika menandatangani token.
Ketika pengguna masuk menggunakan token, periksa last_key_used di DB untuk mencocokkan yang ada di token.
Kemudian, ketika pengguna melakukan logout misalnya, atau jika Anda ingin membatalkan token, cukup ubah field "last_key_used" ke nilai acak lain dan setiap pemeriksaan berikutnya akan gagal, karenanya memaksa pengguna untuk masuk dengan pengguna dan meneruskan lagi.
sumber
Simpan daftar dalam-memori seperti ini
Jika token Anda kedaluwarsa dalam satu minggu, bersihkan atau abaikan catatan yang lebih lama dari itu. Simpan juga hanya catatan terbaru dari setiap pengguna. Ukuran daftar akan tergantung pada berapa lama Anda menyimpan token Anda dan seberapa sering pengguna mencabut token mereka. Gunakan db hanya ketika tabel berubah. Muat tabel dalam memori saat aplikasi Anda mulai.
sumber
------------------------ Agak terlambat untuk jawaban ini tetapi mungkin akan membantu seseorang ------------- -----------
Dari Sisi Klien , cara termudah adalah menghapus token dari penyimpanan browser.
Tapi, Bagaimana jika Anda ingin menghancurkan token di server Node -
Masalah dengan paket JWT adalah bahwa ia tidak menyediakan metode atau cara untuk menghancurkan token. Anda dapat menggunakan metode yang berbeda sehubungan dengan JWT yang disebutkan di atas. Tapi di sini saya pergi dengan redw-jwt.
Jadi untuk menghancurkan token di sisi server Anda dapat menggunakan paket jwt-redis bukan JWT
Perpustakaan ini (jwt-redis) sepenuhnya mengulangi seluruh fungsi perpustakaan jsonwebtoken, dengan satu tambahan penting. Jwt-redis memungkinkan Anda untuk menyimpan label token di redis untuk memverifikasi validitas. Tidak adanya label token di redis membuat token tidak valid. Untuk menghancurkan token di jwt-redis, ada metode penghancuran
ini bekerja dengan cara ini:
1) Instal jwt-redis dari npm
2) Untuk Membuat -
3) Untuk memverifikasi -
4) Menghancurkan -
Catatan : Anda dapat memberikan kadaluwarsa saat masuk token sama seperti yang disediakan di JWT.
Mungkin ini akan membantu seseorang
sumber
Mengapa tidak menggunakan klaim jti (nonce) dan menyimpannya dalam daftar sebagai bidang catatan pengguna (tergantung db, tetapi paling tidak daftar yang dipisahkan koma baik-baik saja)? Tidak perlu pencarian terpisah, karena orang lain telah menunjukkan mungkin Anda ingin mendapatkan catatan pengguna, dan dengan cara ini Anda dapat memiliki beberapa token yang valid untuk contoh klien yang berbeda ("logout di mana-mana" dapat mengatur ulang daftar menjadi kosong)
sumber
Untuk validasi token, periksa waktu kadaluwarsa token terlebih dahulu dan kemudian blacklist jika token tidak kadaluarsa.
Untuk kebutuhan sesi yang panjang, harus ada mekanisme untuk memperpanjang waktu kadaluwarsa token.
sumber
Terlambat ke pesta, dua sen MY diberikan di bawah ini setelah beberapa penelitian. Selama logout, pastikan hal-hal berikut terjadi ...
Bersihkan penyimpanan / sesi klien
Perbarui tabel pengguna waktu-masuk terakhir dan waktu-masuk log setiap kali masuk atau keluar terjadi masing-masing. Jadi waktu tanggal masuk harus selalu lebih besar dari logout (Atau biarkan nol tanggal logout jika status saat ini login dan belum keluar)
Ini jauh lebih sederhana daripada menyimpan daftar tambahan dan membersihkan secara teratur. Dukungan beberapa perangkat memerlukan tabel tambahan untuk tetap masuk, tanggal keluar dengan beberapa detail tambahan seperti OS-atau detail klien.
sumber
Unik per string pengguna, dan string global di-hash bersama-sama
untuk melayani sebagai bagian rahasia JWT memungkinkan pembatalan token individu dan global. Fleksibilitas maksimum dengan biaya pencarian / baca db selama permintaan auth. Juga mudah untuk di-cache, karena jarang berubah.Ini sebuah contoh:
misalnya penggunaan, lihat https://jwt.io (tidak yakin mereka menangani rahasia dinamis 256 bit)
sumber
Saya melakukannya dengan cara berikut:
unique hash
, lalu simpan dalam redis dan JWT Anda . Ini bisa disebut sesiJadi, ketika pengguna masuk, hash unik dibuat, disimpan dalam redis dan disuntikkan ke JWT Anda .
Saat pengguna mencoba mengunjungi titik akhir yang dilindungi, Anda akan mengambil hash sesi unik dari JWT Anda , kueri redis dan lihat apakah itu cocok!
Kami dapat memperpanjang dari ini dan membuat JWT kami lebih aman, berikut caranya:
Setiap X meminta JWT tertentu telah dibuat, kami menghasilkan sesi unik baru, menyimpannya di JWT kami , dan kemudian daftar hitam yang sebelumnya.
Ini berarti bahwa JWT terus berubah dan berhenti JWT basi sedang diretas, dicuri, atau sesuatu yang lain.
sumber
aud
danjti
klaim di JWT, Anda berada di jalan yang benar.Jika Anda ingin dapat mencabut token pengguna, Anda dapat melacak semua token yang diterbitkan pada DB Anda dan memeriksa apakah mereka valid (ada) pada tabel seperti sesi. Kelemahannya adalah Anda akan mendapatkan DB pada setiap permintaan.
Saya belum mencobanya, tetapi saya menyarankan metode berikut untuk memungkinkan pencabutan token sambil menjaga DB hit ke minimum -
Untuk menurunkan tingkat pemeriksaan basis data, bagi semua token JWT yang dikeluarkan ke dalam grup X berdasarkan beberapa asosiasi deterministik (misalnya, 10 grup dengan digit pertama dari id pengguna).
Setiap token JWT akan menyimpan id grup dan cap waktu yang dibuat saat pembuatan token. misalnya,
{ "group_id": 1, "timestamp": 1551861473716 }
Server akan menyimpan semua id grup di memori dan setiap grup akan memiliki cap waktu yang menunjukkan kapan peristiwa log-out terakhir dari pengguna yang termasuk dalam grup itu. misalnya,
{ "group1": 1551861473714, "group2": 1551861487293, ... }
Permintaan dengan token JWT yang memiliki stempel waktu grup yang lebih lama, akan diperiksa validitasnya (hit DB) dan jika valid, token JWT baru dengan stempel waktu baru akan dikeluarkan untuk penggunaan klien di masa mendatang. Jika cap waktu grup token lebih baru, kami mempercayai JWT (No DB hit).
Jadi -
sumber
Jika opsi "logout dari semua perangkat" dapat diterima (dalam banyak kasus itu adalah):
Perjalanan db untuk mendapatkan catatan pengguna dalam banyak kasus tetap diperlukan sehingga ini tidak menambah banyak biaya overhead untuk proses validasi. Tidak seperti mempertahankan daftar hitam, di mana beban DB signifikan karena perlunya menggunakan gabungan atau panggilan terpisah, bersihkan catatan lama dan sebagainya.
sumber
Saya akan menjawab Jika kita perlu menyediakan fitur logout dari semua perangkat ketika kita menggunakan JWT. Pendekatan ini akan menggunakan pencarian basis data untuk setiap permintaan. Karena kita memerlukan status keamanan yang gigih bahkan jika ada server crash. Di tabel pengguna kita akan memiliki dua kolom
Setiap kali ada permintaan keluar dari pengguna, kami akan memperbarui LastValidTime ke waktu saat ini dan Masuk-ke palsu. Jika ada permintaan masuk, kami tidak akan mengubah LastValidTime tetapi Logged-In akan disetel ke true.
Ketika kita membuat JWT kita akan memiliki waktu pembuatan JWT di payload. Ketika kami mengotorisasi layanan, kami akan memeriksa 3 ketentuan
Mari kita lihat skenario praktis.
Pengguna X memiliki dua perangkat A, B. Ia masuk ke server kami pada pukul 19:00 menggunakan perangkat A dan perangkat B. (katakanlah JWT, waktu kedaluwarsa adalah 12 jam). A dan B keduanya memiliki JWT dengan CreatedTime: 19:00
Pada jam 9 malam ia kehilangan perangkat B. Ia segera keluar dari perangkat A. Itu berarti Sekarang entri pengguna basis data X kami memiliki LastValidTime sebagai "ThatDate: 9: 00: xx: xxx" dan Masuk dengan "Salah".
Pada jam 9:30 Mr.Thief mencoba masuk menggunakan perangkat B. Kami akan memeriksa basis data meskipun Log-In itu salah sehingga kami tidak akan mengizinkannya.
Pada jam 10 malam, Mr.X masuk dari perangkatnya A. Sekarang perangkat A memiliki JWT dengan waktu yang dibuat: 10 malam. Sekarang database Logged-In diatur ke "true"
Pukul 10.30 malam Mr.Thief mencoba masuk. Meskipun Logged-in benar. LastValidTime adalah pukul 9 malam dalam basis data tetapi B's JWT telah membuat waktu sebagai jam 19:00. Jadi dia tidak akan diizinkan untuk mengakses layanan. Jadi menggunakan perangkat B tanpa memiliki kata sandi yang tidak bisa dia gunakan sudah dibuat JWT setelah satu perangkat keluar.
sumber
Solusi IAM seperti Keycloak (yang telah saya kerjakan) menyediakan titik akhir Token Revocation
Titik Akhir Pencabutan Token
/realms/{realm-name}/protocol/openid-connect/revoke
Jika Anda hanya ingin keluar dari agen pengguna (atau pengguna), Anda bisa memanggil titik akhir juga (ini hanya akan membatalkan Token). Sekali lagi, dalam kasus Keycloak, Partai Bergantung hanya perlu memanggil titik akhir
/realms/{realm-name}/protocol/openid-connect/logout
Tautkan jika Anda ingin mempelajari lebih lanjut
sumber
Tampaknya ini sangat sulit untuk diselesaikan tanpa pencarian DB pada setiap verifikasi token. Alternatif yang dapat saya pikirkan adalah menyimpan daftar hitam dari sisi server token yang tidak valid; yang harus diperbarui pada database setiap kali terjadi perubahan untuk mempertahankan perubahan di restart, dengan membuat server memeriksa database setelah restart untuk memuat daftar hitam saat ini.
Tetapi jika Anda menyimpannya di memori server (semacam variabel global) maka itu tidak akan dapat diskalakan di beberapa server jika Anda menggunakan lebih dari satu, jadi dalam hal ini Anda dapat menyimpannya di cache Redis bersama, yang seharusnya set-up untuk mempertahankan data di suatu tempat (database? filesystem?) dalam kasus itu harus di-restart, dan setiap kali server baru berputar itu harus berlangganan cache Redis.
Alternatif untuk daftar hitam, menggunakan solusi yang sama, Anda dapat melakukannya dengan hash yang disimpan dalam redis per sesi karena jawaban lain ini menunjukkan (tidak yakin itu akan lebih efisien dengan banyak pengguna yang masuk).
Apakah terdengar sangat rumit? itu untuk saya!
Penafian: Saya belum pernah menggunakan Redis.
sumber
Jika Anda menggunakan aksioma atau lib permintaan http berbasiskan janji yang serupa, Anda dapat menghancurkan token di bagian depan di
.then()
bagian dalam. Ini akan diluncurkan di respons .then () bagian setelah pengguna menjalankan fungsi ini (kode hasil dari titik akhir server harus ok, 200). Setelah pengguna mengklik rute ini saat mencari data, jika bidang basis datauser_enabled
salah maka akan memicu token yang merusak dan pengguna akan segera log-off dan berhenti mengakses rute / halaman yang dilindungi. Kami tidak harus menunggu token untuk kedaluwarsa saat pengguna masuk secara permanen.sumber
Saya hanya menyimpan token to tables pengguna, ketika pengguna login saya akan memperbarui token baru, dan ketika auth sama dengan pengguna saat ini jwt.
Saya pikir ini bukan solusi terbaik tetapi itu bekerja untuk saya.
sumber
Stateless JWT
danStateful JWT
(yang sangat mirip dengan sesi).Stateful JWT
dapat mengambil manfaat dari mempertahankan daftar putih token.