3 dk, tahmini okuma süresi
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.
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"
}
}
"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))
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
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
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.
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 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.
Ş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