3 dk, tahmini okuma süresi

JWT ve Node ile Kimlik Doğrulama

En Son Güncelleme: 25 Nisan, 2022

Kimlik doğrulama ve yetkilendirme (auth), özellikle yeni başlayan biriyseniz çok göz korkutucu olabiliyor. En azından benim için durum böyleydi. Bu gönderiyle, pasaport.js ile jwt auth süreci hakkında temel bilgiler edinebilirsiniz. Projeyi mümkün olduğu kadar basit tutmak için hiçbir framework kullanmayacağız.

§Kullanacağımız Paketler

Parolamızı karma bir hale getireceğiz (hashing) ve kullanıcı için oluşturduğumuz "token" yerel depolamada (local storage) saklanacak. Bu proje için bir veritabanı da kullanmamız gerekecek, benim kişisel tercihim MongoDB Atlas. Kullanımı gayet kolay. Veritabanıyla iletişime geçmek için de mongoose paketini kullanacağız. "nodemon" paketiyle de projemizi her değişikliten sonra tekrar başlatmamıza gerek kalmayacak.

Bir npm paketi oluitur ve bütün soruları default değerlerle geçiştir ("-y" ekiyle):

npm init -y

Ayrıca proje için gerekli paketleri de bu komutla kurabliriz:

npm i bcrypt crypto dotenv express fs jsonwebtoken mongoose passport nodemon

Don't forget to add a script for nodemon, if you don't wanna write the command on the console again and again, mine is "dev": "nodemon server.js".

nodemon komutunu da package.json dosyasında script bölümüne eklemeyi unutma. Script'e bu şekilde ekleyebilirsiniz "dev": "nodemon server.js"

package.json dosyasındaki tüm kod şu şekilde:

{ "name": "auth-project-post", "version": "1.0.0", "description": "", "main": "server.js", "scripts": { "dev": "nodemon server.js", "test": "echo \"Error: no test specified\" && exit 1", "start": "node server.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "bcrypt": "^5.0.1", "crypto": "^1.0.1", "dotenv": "^16.0.0", "express": "^4.17.3", "fs": "^0.0.1-security", "jsonwebtoken": "^8.5.1", "mongoose": "^6.3.0", "passport": "^0.5.2" }, "devDependencies": { "nodemon": "^2.0.15" } }

§Server Dosyamızı Oluşturalım

"npm run dev" komutumuz nodemon'a server.js dosyasını okumasını soyleyecek. Projeninizin root (yani dokumanların bulunduğu ilk dosyada) server.js dosyanızı oluşturun ve aşağıdaki kodu kopyalayın.

const express = require('express'); // .env dosyamızdan veritabanı adresimizi alıyoruz (MongoDB URL), bağlantı için gerekli require('dotenv').config(); const mongoose = require('mongoose'); // Routeları ayırmakta fayda var, çok daha kolay anlaşılır görürünüyor const routes = require('./routes/index.js') const secretRoutes = require('./secret-routes/index.js') const app = express(); // data dondermek için body'i kullanacağız app.use(express.json()) app.use(express.urlencoded({"extended":true})) // Static dosyalarınızın nereden sunulacağını express'e söylemeniz gerekiyor app.use('/', express.static(process.cwd() + '/public')); app.use('/', routes); app.use('/secret', secretRoutes); const port = process.env.PORT || 5000; // Risk alamamak adına "listen" komutunu veritabanı bağlantısı gerçekleştikten sonraya saklıyoruz mongoose.connect(process.env.URI, { useNewUrlParser:true, useUnifiedTopology: true, } ) .then(() => app.listen(port, () => console.log(`Uygulama bağlantı noktasında dinliyor: ${port}`))) .catch((error) => console.log(error))

§Veritabanı Bağlantısını ve Kullanıcı Şemasını Halledelim

Bu proje için kullandığım veritabanı Mongodb Atlas. MongoDB Atlas projeleriniz için kullanabileceğiniz ücretsiz bir plan sağlıyor. Kendi veritabanınızı oluşturmak için bir kılavuz burada bulabilirsiniz. "mongoose" paketi, veritabanınıza bağlanmayı ve etkileşim kurmayı çok daha kolaylaştırıyor. Bu projede mongoose kullanacağım. Ayrıca Mongodb URL'mi proje kökümde bir .env dosyasında tutuyorum.

Konsolda "npm run dev" komutunu çalıştırarak bağlantınızı test edebilirsiniz. Her şey yolundaysa, "Uygulama bağlantı noktasında dinliyor: 5000" ifadesini görmelisiniz.

Tamam, şimdi kullanıcı şemamızı (Schema) oluşturalım. Şemayı, veritabanına onunla nasıl etkileşim kurmak istediğinizi bildirmenin bir yolu olarak düşünün. Tam olarak ne tür verileri kaydetmek istediğinizi açıklıyorsunuz. Projenizin kök dizininde bir "models" klasörü oluşturun, bu klasörde bir model oluşturabiliriz. Projemiz için bir "user.js" dosyasına ihtiyacımız var. Kod burada:

const mongoose = require('mongoose'); const { Schema } = mongoose; const UserSchema = new Schema({ email: { type: String, required: true }, password: { type: String, required: true } }) const User = mongoose.model("User", UserSchema); module.exports = User

§Anahtarları Oluşturma Zamanı

Kökte (Root) proje, genel (public) ve özel (private) anahtarları oluşturmak için bir dosya oluşturun. Private key ile tokenları imzalayacağız (sign()). Kullanıcıların ulaşabiliceği kısımlarda private key kesinlikle görünmemeli. Public key ile de kullanıcıların kimliklerini doğrulayacağız (verify). Devam edin ve bu anahtarları oluşturacağınız dosyanızı proje kökünde oluşturun. Ben benim dosyama genKeyPair.js adını verdim. Bütün kodu burada bulabilirsiniz:

const fs = require('fs'); const crypto = require('crypto'); const genKeyPair = () => { const keyPair = crypto.generateKeyPairSync('rsa', { modulusLength: 4096, publicKeyEncoding: { type: 'pkcs1', format: 'pem' }, privateKeyEncoding: { type: 'pkcs1', format: 'pem' } }); // Anahtarlar için ayrı dosyalar oluşturulup kaydedilecek fs.writeFileSync(__dirname + '/id_rsa_pub.pem', keyPair.publicKey); fs.writeFileSync(__dirname + '/id_rsa_priv.pem', keyPair.privateKey) } genKeyPair();

Şimdi de bu komutla anahtarlarınızı oluşturun:

node genKeyPair.js

§Üye Olumları Halledelim

Dosyalarımız çok basit olacak, ancak istediğiniz her türlü eklemeyi yapabilirsiniz. Proje kökümüzde bi klasör oluşturacağız (public) ve bu klasör içinde de bir js klasörü ve css dosyası oluşturacağız. (Bir hatırlatma, kaçırdığınız bir yer olursa endişe etmeyin projeye ait bütün kodu burada bulabilirsiniz). Gerekli mantığı bu javascript dosylararımızda oluşturacağız ve css dosyamızla da güzelleştireceğiz. Daha iyi yapabileceğiniz bir yer olduğunu düşünüyorsanız kesinlike devam edin ve kodu değiştirin. İylişetirmeye açık bir çok bölüm var.

Public klasörünüzün içinde oluşturacağınız "home.html" dosyasında css ve javascript dosyalarınızın referanslarını unutmayın. Kodu aşağıda bulabilirsiniz:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" type="text/css" href="style.css"> <title>Document</title> </head> <body> <nav> <a href="/">HOME</a> <ul> <li><a href="register">Register</a></li> <li><a href="/login">Login</a></li> <li><a href="/secret">Secret Page</a></li> </ul> </nav> <div class="home-wrapper"> <h1>Welcome to he home page of our awesome website</h1> <p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Sint eum et quam excepturi corrupti, ipsa illum eligendi aut! Labore molestiae fuga incidunt. Recusandae, consequatur consectetur? Aliquam eligendi deserunt doloribus voluptatum. Eligendi rem repellendus dolores laboriosam dolorum quia vero nihil perferendis id odio, quas molestiae, vitae exercitationem autem voluptates! Eius illum aliquid tenetur sapiente minus quo, consequatur magni repudiandae. Quaerat, necessitatibus! Ea soluta repellendus modi ab officia quae porro dolorem cum nulla. Aspernatur dolore tenetur corporis deserunt consequuntur omnis voluptate tempora eos facilis ipsum. Aspernatur ad laudantium, harum dolor autem eius. Reprehenderit, aliquam distinctio. Vel tenetur dolore praesentium fugit nam iste totam amet nulla voluptatibus. Eaque reiciendis reprehenderit, eos suscipit omnis id at vel accusantium earum explicabo, delectus dignissimos possimus incidunt! Aperiam optio perferendis natus, vel tempore quaerat assumenda maxime voluptate voluptates rerum suscipit? Asperiores, ea deleniti! Amet similique, provident ipsam error ad nostrum corporis, eligendi ea dignissimos dicta, quia aperiam?</p> </div> </body> </html>

Kullanacağımız css gayet basit:

* { margin: 0; padding: 0; box-sizing: border-box; } body { display: flex; flex-direction: column; align-items: center; background: rgb(94, 139, 162); color: rgb(255, 255, 255); } p { line-height: 1.7; padding-top: 20px; } .home-wrapper { width: 80%; padding: 50px; background: rgba(0,0,0,0.7); margin: 10px; border-radius: 20px; } nav { background: olivedrab; display: flex; flex-direction: row; align-items: flex-start; width: 100%; padding: 20px; } ul { margin-left: auto; display: flex; flex-direction: row; list-style: none; } li { margin-left: 10px; } a { color: azure; font-size: 1.2rem; text-decoration: none; } .login-wrapper, .register-wrapper { margin-top: 20px; background: rgba(0,0,0,0.7); padding: 20px; border-radius: 20px; } .login-form, .registery-form { display: flex; flex-direction: column; min-width: 600px; } .login-form > input, .registery-form > input { padding: 20px; font-size: 1.6rem; border-radius: 20px; } .login-submit-btn, .registry-submit-btn { margin-top: 10px; border:none; background: rgba(234, 97, 170, 0.7); transition: 0.4s; } .login-submit-btn:hover, .registry-submit-btn:hover { cursor: pointer; background: rgba(234, 97, 170, 1); } .secret-wrapper { width: 80%; margin-top: 20px; background: rgba(0,0,0,0.7); padding: 50px; border-radius: 20px; }

Şimdi de "register.html" dosyamızı oluşturalım ve kullanıcıları kaydetmeye başlayalım. Bütün gerekli mantık javascript dosyasında halledilecek:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" type="text/css" href="style.css"> <script defer src="./js/registry.js"></script> <title>Document</title> </head> <body> <nav> <a href="/">HOME</a> <ul> <li><a href="register">Register</a></li> <li><a href="/login">Login</a></li> <li><a href="/secret">Secret Page</a></li> </ul> </nav> <div class="register-wrapper"> <h1>Create an account</h1> <p class="register-error"></p> <form class="registery-form"> <p>Email:</p> <input placeholder="example@gmail.com" name="email" id="femail" type="email" > <p>Password:</p> <input id="fpassword" name="password" type="password" > <p>Rewrite Password:</p> <input id="fpassword2" name="password2" type="password" > <input class="registry-submit-btn" type="submit" value="SUBMIT"> </form> </div> </body> </html>

Kullanıcı kaydetmek için gerekli mantık "registry.js" dosyamızda halledilecek:

const regForm = document.querySelector('.registery-form'); const password = document.querySelector('#fpassword'); const password2 = document.querySelector('#fpassword2'); const email = document.querySelector('#femail'); regForm.addEventListener('submit', async (event) => { event.preventDefault() // sayfa yenilenmesini önle try { // Kullanıcı bilgilerini body'de gönder const response = await fetch('/register', { method: 'POST', body: JSON.stringify({ email: email.value, password: password.value, password2: password2.value }), headers: { 'Content-Type': 'application/json' }, }); const data = await response.json(); // Oluşturulmuş token'i al const { expiresIn } = await data; const { token } = await data // Token'ı sil localStorage.removeItem("id_token"); localStorage.removeItem("expires_at"); // Token'e 24 saatlik bir geçerlilik süresi veriyoruz const expiresAt = new Date.now() + Number.parseInt(expiresIn) * 86400 * 1000; // day // Local storage'a token'ı kaydediyoruz localStorage.setItem('id_token', token); localStorage.setItem('expires_at', JSON.stringify(expiresAt.valueOf())); // Bu noktaya kadar herşey olundaysa kullanıcıyı anasayfaya yönlendir if(data){ window.location = '/'} } catch (error) { // Uygulamınızı etkilemeden önce hatayı yakalayın console.log("error in registry ==>",error) } })

HTML kısmını gördüğümüze göre, kullanıcı bilgilerini nasıl backend'de (node) kullancağımızı da görebiliriz. Gönderilen kullanıcı bilgileri ile token oluşturulacak ve vu token daha sonraki aşamalarda kullanıcı yetkilendirmeleri için kullanılabilecek.

Proje kökünde "routes" klasörü oluşturun ve bu klasörün içinde "index.js" dosyasını oluşturun. Bu dosya içinde backend mantığımızı tanımlayacağız:

const express = require('express'); const jsonwebtoken = require('jsonwebtoken'); const router = express.Router(); const path = require('path'); const bcrypt = require('bcrypt'); const fs = require('fs'); const passport = require('passport'); const User = require('../models/user.js') // Private key jwt oluşturmak için kullanılacak const pathToKey = path.join(__dirname, "../", 'id_rsa_priv.pem') const PRIV_KEY = fs.readFileSync(pathToKey, 'utf8'); // Jwt oluşturma fonksiyonumuz const issueJWT = (user) => { const _id = user._id; const expiresIn = '1'; const payload = { sub: _id, iat: Date.now() }; const signedToken = jsonwebtoken.sign(payload, PRIV_KEY, { expiresIn: expiresIn, algorithm: 'RS256' }); return { token: "Bearer " + signedToken, expires: expiresIn } // Bu kadar kolay } // Bu fonksiyon ile kullanıcı şifrelerini karma bi hale getireceğiz (hashing) const genPassword = async (plaintextPassword) => { const saltRounds = 10; const hash = await bcrypt.hash(plaintextPassword, saltRounds) return hash }; // Anasayfamızı burdan sunuyoruz router.get('/', (req, res) => { res.sendFile(path.dirname(__dirname) + "/public/home.html"); }) // Kayıt formumuz da burada router.get('/register', (req, res) => { res.sendFile(path.dirname(__dirname) + "/public/register.html"); }) // Post bu adrese gelecek ve burada gerekli mantık üretilecek router.post('/register', (req, res) => { const { email, password, password2 } = req.body; // Gelen bilgilere bir bakalım console.log("these are recieved", email, password, password2) // Email gelmememişse if (!req.body.email) { console.log("No info sent") return } // Şifreler eşleşmezse if (password !== password2) { res.status(401).json({ error: "Passwords are not identical!" }) return } // Kullanıcı emailini veritabanında arıyoruz User.findOne({ email: email}, async (err, data) => { if(err) { return res.status(400).json({ error: `an error occured: ${err}` }) } else if(data) { // Kullanıcı zaten kayıtlıysa return res.status(400).json({ error: "User exists in database" }) } else { // Kullanıcı veritabanında yoksa // Şifreyi olduğu gibi kaydetmek yerine hash'leyerek // kaydetmek çok daha sağlıklı, kullanıcı şifrelerini // kullanıcıdan başka kimsenin görmemesi lazım const passwordToSave = await genPassword(password); // Kullanıcıyı oluştur const newUser = new User({ email: email, password: passwordToSave }); // Veritabanına kullanıcıyı kaydediyoruz sonunda newUser.save() .then(async user => { console.log("Kullanıcıyı kaydettik") // Kaydettiğimiz kullanıcı id'siyle token'i oluştur const jwt = await issueJWT(user) // Bilgileri gönder res.status(201).json( { user: { email: user.email, }, token: jwt.token, expiresIn: jwt.expires } ).catch(console.log) // Bir hata durumunda }) } }) }) module.exports = router;

İşte bu kadar! Kullanıcı oluşturuyoruz ve üye girişini de otomatik olarak yapmış oluyoruz.

§Gelelim Üye Girişine

Kullanıcılarımız elbette giriş yapabilmeli. HTML dosyamızı oluştururak başlıyoruz:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" type="text/css" href="style.css"> <script defer src="./js/login.js"></script> <title>Document</title> </head> <body> <nav> <a href="/">HOME</a> <ul> <li><a href="register">Register</a></li> <li><a href="/login">Login</a></li> <li><a href="/secret">Secret Page</a></li> </ul> </nav> <div class="login-wrapper"> <h1>Login</h1> <p class="login-error"></p> <form class="login-form"> <p>Email:</p> <input type="email" id="l-email" name="email" placeholder="example@gmail.com" > <p>Password:</p> <input type="password" id="l-password" name="password" > <input class="login-submit-btn" type="submit" value="LOGIN"> </form> </div> </body> </html>

Ve şimdi üye giriş formu için gerekli mantığımızı oluşturuyoruz (js klasörü içinde login.js), mantık registry için oluşturduğumuz javascript dosyasına çok benziyor:

const loginForm = document.querySelector('.login-form'); const email = document.querySelector('#l-email'); const password = document.querySelector('#l-password'); loginForm.addEventListener('submit', async (event) => { event.preventDefault(); try { const response = await fetch('/login', { method: 'POST', body: JSON.stringify({ email: email.value, password: password.value }), headers: { 'Content-Type': 'application/json' }, }) const data = await response.json(); const { expiresIn } = await data; const { token } = await data localStorage.removeItem("id_token"); localStorage.removeItem("expires_at"); const expiresAt = Date.now() + Number.parseInt(expiresIn) * 86400 * 1000; // day localStorage.setItem('id_token', token); localStorage.setItem('expires_at', JSON.stringify(expiresAt.valueOf())); if(data){ window.location = '/'} } catch (error) { console.log("üye girişinde hata: ", error) } })

routes/index.js dosyamızdan login için gerekli olan HTML sayfamızı sunuyoruz (gönderiyoruz). Aşağıdaki kodu ekleyerek gerekli mantığı da halledebiliriz kolayca:

// Öncelikle .html dosyamızı gönderiyoruz router.get('/login', (req, res) => { res.sendFile(path.dirname(__dirname) + "/public/login.html"); }) router.post('/login', (req, res) => { // Gerekli olan bilgileri request'ten alıyoruz const { email,password } = req.body; if(!email) { // İsterseniz daha açıklayıcı bir mesaj yazabilir veya // kullanıcıya bir açıklama yapabilirsiniz // ben basitce console.log kullandım console.log("Bilgi gönderilmedi") return } // Veritabanında kullınıcıyı arıyoruz User.findOne({ email: email}, async (err, user) => { if(err){ return res.status(400).json({ error: `Bir hata oluştu: ${err}`}) } else if (!user) { // Kullanıcı yoksa res.status(400).json({ error: "unknown username"}) } else { // Kullanıcıyı bulduk şimde şifreleri karşılaştıralım const valid = await bcrypt.compare(password, user.password); // Şifreler eşleşmezse if (!valid) { console.log("Şifreler farklı!") res.status(400).json({ error: "Yanlış şifre"}) } else { // Şimdiye kadar bir hata yoksa const tokenObj = issueJWT(user) res.status(200).json({ user: { username: user.username, email: user.email, createdAt: user.createdAt }, token: tokenObj.token, expiresIn: tokenObj.expires } ); } } }) });

Bir kullanıcı oluşturabilir ve üye girşi sağlayabliriz artık. Hadi bunları kullanarak bir şey yapalım.

§Kullanıcılara Özel bir Sayfa

Kullanıcılara özel gizli bir sayfa olşturabilir ve gizli bilgileri sadece kullanıcılarınızla paylaşabilirsiniz. Ben çok daha basit bir şey yaptım. Kullanıcı kayıtlı ise ekranda bir metin belirecek sadece.

Siz node ve passport.js'i kullanarak bir middleware oluşturabilir ve daha güvenli bir sistem oluşturabilirsiniz. Kullanıcılarınız bu sayfayı gu şekilde kaydolmadan veya giriş yapmadan göremeyecektir.

Bilgileri yerel depolamadan (local storage) alıyoruz ve doğrulamayı bu şekilde gerçekleştiriyoruz, istediğiniz mantığı burada da ekleyebilirsiniz:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" type="text/css" href="style.css"> <script defer src="./js/profile.js"></script> <title>Document</title> </head> <body> <nav> <a href="/">HOME</a> <ul> <li><a href="register">Register</a></li> <li><a href="/login">Login</a></li> <li><a href="/secret">Secret Page</a></li> </ul> </nav> <div class="secret-wrapper"> <div id="user-email"></div> </div> </body> </html>

Ve sonunda javascript dosyamız ile gerekli mantığı oluşturuyoruz:

const profileEmail = document.querySelector("#user-email"); const idToken = localStorage.getItem("id_token"); const expiresAt = localStorage.getItem("expires_at"); // Token'in tarihi geçmiş mi bir bakalım if(idToken && expiresAt >= Date.now()){ const text = "Token hala geçerli, sitemize hoş geldiniz" const h1 = document.createElement("h1") h1.innerText = text profileEmail.appendChild(h1) }

Proje kökünde secret-routes klasörü içerisnde kullanıcılara özel sayfalarımızı sunacağız

Burada ben sadece .html dosyamızı sunuyorum. jwt ile ilgili eklenebilecek o kadar çok şey var ki, eklemek istediğiniz bir şey varsa devam edip ekleyin.

You can handle the autharization here using passport package. You can modify the app so the client sends the token in the autharization header and in the backend, check the validity using the public key we created. I'm not including that part here because I know from experince that we only learn after we struggle to understand the logic behind the technologies we use. Give this page a look. It won't take long and trust me you will thank me afterwards.

Pasaport paketini kullanarak yetkilendirmeyi burada halledebilirsiniz. Burada uygulamanızı değiştirerek authorisation header'ında tokeni göderebilir ve token'in gereçliliğini burada kontrol edebilirsiniz. O kısmı buraya eklemiyorum çünkü deneyimlerden biliyorum ki, ancak kullandığımız teknolojilerin arkasındaki mantığı anlamak için çaba gösterdikten sonra gerçekten öğreniyoruz.

§Sonuç ve Sonraki Adımlar

Şimdiye kadar kullanıcı oluşturmayı, şifreleri hash'lemeyi, public ve private anahtarlar oluşturmayı öğrendik. Baya da bir şey öğrenmişiz.

Tüm bunların birlikte nasıl çalıştığını görmek, sizi daha iyi ve kendinden emin bir developer yapacaktır. Benim için durum kesinlikle öyleydi. Çok makul fiyatlara auth süreçleriyle ilgilenen çok sayıda hizmet olmasına rağmen, perde arkasında neler olup bittiğini bilmek her zaman iyidir.

Ilker Akbiyik