Express.js ve Twilio Verify ile iki faktörlü kimlik doğrulama
Express.js, EJS, MongoDB ve Twilio Verify kullanarak SMS ile tek kullanımlık şifre doğrulaması eklediğim örnek uygulama.
Güncelleme:
Bu, 2022’de hazırladığım bir demo. Gerçek bir uygulamada oturumlar için kalıcı bir session store, güvenli çerezler, CSRF koruması ve doğrulama istekleri için hız sınırı eklemek gerekir. Paketleri kurarken kullanılan sürümleri reponun kilit dosyasından kontrol et.
OTP Nedir?
Tek seferlik şifre (OTP), yalnızca bir oturum veya işlem için geçerli olan bir şifre veya PIN’dir.
OTP’ler, geleneksel şifreleri değiştirmek veya iki faktörlü kimlik doğrulama (2FA) yaklaşımını kullanarak şifreleri güçlendirmek için kullanılabilir.
Makale Sonunda Neler Öğreneceğiz?
- Web uygulamalarında kimlik doğrulamanın temellerini
- Express.js ile node üzerinde çalışan bir sunucu ortamı oluşturmayı
- MongoDB’yi Express projemize entegre etmeyi
- EJS şablonlama motorunu kullanarak uygulamamızda kullanıcıya görünecek olan arayüzler oluşturmayı
- Passport paketiyle basit Authentication yapmayı
- Twilio Verify’ı kullanarak api rotalarımızı korumayı — Protect Route lar oluşturmayı
Proje Gereksinimleri
- Node.js
- Veritabanı (MongoDB)
- Kod Editör (VS Code)
- Temel Seviyede HTML, CSS, JavaScript, Express.js bilgisi
Projemizin detaylarına girmeden önce birkaç önemli bilgiden bahsetmek istiyorum.
Authentication ve Authorization Nedir?
Authentication (Kimlik doğrulama), bir kullanıcıyı tanımlama ve bir kullanıcının uygulamamızda bir hesabı olduğunu doğrulama sürecinin tamamıdır.
Authorization (Yetkilendirme), en temelde, tamamen kullanıcı izinleriyle ilgilidir. Yani kullanıcının uygulamada yapmasına izin verilenler api istekleri diyebiliriz. Authorization, aslında protect route kavramıyla da ilgilisi vardır. Bunun en belirgin örneği ise admin kullanıcısının tüm ekleme silme düzenleme işlemleri yani kısaca GET, POST, PUT, DELETE istek metodlarının hepsine izni vardır ve apideki tüm rotalara erişebilir. Fakat öte yandan normal bir kullanıcı bu isteklere izni olmadığı için erişemez.
Authentication ile authorization arasındaki fark
Authentication: Kim olduğunla ilgilenir. Kullanıcı kimliğini doğrulama sürecidir.
Authorization: Kullanıcı uygulama genelinde hangi apilere istek atabilir hangisine atamaz bunun belirlendiği aşamadır.
Bir kullanıcının kimliğini doğrulamanın en yaygın yolu kullanıcı adı ve paroladır.
Uygulamamızı Kurma
mkdir authWithTwilioVerify
cd authWithTwilioVerify
npm init -y
Express sunucusu oluşturma
Model Görünüm Denetleyicisi (MVC) mimarisine uygun olarak aşağıdaki kod ile klasörlerimizi oluşturalım.
mkdir public controllers views routes config models
Klasör yapısı hakkında detaylar
Controllersuygulama rotalarının kontrollerini tutar.Modelsveritabanı modellerimizi tutar.Publicstatik varlıklarımızı tutar; CSS dosyaları, resimler v.b.Viewstarayıcıda oluşturulacak sayfaları içerir.Routesuygulamamızın farklı yollarını tutar.Configuygulamamıza özgü bilgileri tutar.
Uygulamamızı oluşturmak için aşağıdaki paketleri kurmamız gerekiyor:
nodemondeğişiklik yaptığımızda sunucumuzu otomatik olarak yeniden başlatır.expressrotaları işlemek için bize güzel bir arayüz sağlar.express-sessionekspres uygulamamızda oturumları kolayca işlememize olanak tanır.connect-flashkullanıcılarımıza mesaj göstermemizi sağlar.
npm install nodemon -D
Sunucumuzu nodemon kullanarak başlatmak için aşağıdaki komut dosyasını package.json dosyasına ekleyin.
{
"scripts": {
"dev": "nodemon index"
}
}
Gerekli olan diğer paketleri kuralım.
npm install express express-session connect-flash dotenv
Bir index.js dosyası oluşturun ve uygulamamız için gerekli paketleri ekleyin.
require('dotenv').config();
const path = require('path')
const express = require('express');
const session = require('express-session')
const flash = require('connect-flash')
const port = process.env.PORT || 3000
const app = express();
app.use('/static', express.static(path.join(__dirname, 'public')))
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, sameSite: 'lax', secure: process.env.NODE_ENV === 'production' }
}
));
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
// Connect flash
app.use(flash());
// Global variables
app.use(function(req, res, next) {
res.locals.success_msg = req.flash('success_msg');
res.locals.error_msg = req.flash('error_msg');
res.locals.error = req.flash('error');
res.locals.user = req.user
next();
});
//define error handler
app.use(function(err, req, res, next) {
res.render('error', {
error : err
})
})
//listen on port
app.listen(port, () => {
console.log(`app is running on port ${port}`)
});
Sonrasında uygulamamızı çalıştıralım
npm run dev
MongoDB’yi Express Uygulamamıza Entegre Etme
MongoDB, verileri belge olarak saklar. MongoDB bu belgeleri BSON biçiminde saklar. Node.js kullandığımız için, MongoDB’de depolanan verileri JavaScript nesnelerine dönüştürmek ve bunları işlemek oldukça kolaydır.
MongoDB’yi ekspres uygulamamıza entegre etmek için Mongoose kullanacağız. Mongoose bir ODM’dir (Object Data Mapper).
Aslında Mongoose, Native MongoDB işlevleri etrafında bir sarmalayıcı oluşturarak MongoDB’yi uygulamamızda kullanmamızı kolaylaştırır.
npm install mongoose -- save
index.js dosyamıza aşağıdaki kodu ekleyelim
const app = express()
//connect to mongodb
mongoose.connect('mongodb://localhost:27017/authWithTwilio',
{
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => {
console.log(`connected to mongodb`)
})
.catch(e => console.log(e))
mongoose.connect() işlevi, bağlantı dizesini kullanarak MongoDB veritabanımıza bir bağlantı kurmamızı sağlar.
Bağlantı dizesinin biçimi mongodb://localhost:27017/{database_name} şeklindedir.
mongodb://localhost:27017/ MongoDB’nin varsayılan ana bilgisayarıdır, ve database_name ise veri tabanımızın adıdır.
Modeller dizinimizde kullanıcı modelimizi oluşturuyoruz:
cd models
touch user.js
user.js, mongoose gerektirir ve kullanıcı şemamızı oluşturur:
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name : {
type: String,
required: true
},
username : {
type: String,
required: true
},
password : {
type: String,
required: true
},
phonenumber : {
type: String,
required: true
},
email : {
type: String,
required: true
},
verified: Boolean
})
module.exports = mongoose.model('user', userSchema)
Parolaları hash’leme
Uyarı: Kullanıcıların parolalarını asla düz metin olarak veritabanınızda saklamayın. Parolaları veritabanına göndermeden önce her zaman şifreleyin.
Veritabanında kullanıcının parolası yerine bcrypt hash’i saklanır. Veritabanına erişen biri parolayı doğrudan okuyamaz, ama zayıf parolaları tahmin ederek hash’leri karşılaştırabilir. İkinci faktör, parola kontrolünden ayrı bir adımdır.
bcryptjs, parolaları tek yönlü olarak hash’ler. Girişte hash’i çözmek yerine girilen parolayı bcrypt.compare() ile doğrularız.
npm install bcryptjs
models/user.js, bcryptjs paketi gereklidir.
//after requiring mongoose
const bcrypt = require('bcryptjs')
//before module.exports
//hash password on save
userSchema.pre('save', async function() {
return new Promise( async (resolve, reject) => {
await bcrypt.genSalt(10, async (err, salt) => {
await bcrypt.hash(this.password, salt, async (err, hash) => {
if(err) {
reject (err)
} else {
resolve (this.password = hash)
}
});
});
})
})
userSchema.methods.validPassword = async function(password) {
return new Promise((resolve, reject) => {
bcrypt.compare(password, this.password, (err, res) => {
if(err) {
reject (err)
}
resolve (res)
});
})
}
userSchema.pre('save', callback) , bir mongoose hook ‘udur ve verilerimizi veri tabanına kaydetmeden önce değiştirmemize olanak tanır.
callback function , Geri çağırma işlevinde, oluşturduğumuz bcrypt.genSalt()kullanarak parolayı bcrypt.hash() ile hash’lemeyiyapmaya çalışan bir Promise döndürürüz. Bu hash sırasında bir hata oluşursa reddederiz veya this.password = hash ayarlayarak çözeriz. this.password, userSchema parolasıdır.
Daha sonra, mongoose, schema.methods.method_name kullanarak şemalara yöntemler eklememiz için bir yol sağlar.
Bizim durumumuzda, kullanıcı şifrelerini doğrulamamıza izin veren bir yöntem oluşturuyoruz. ***userSchema.methods.validPassword*’**a bir fonksiyon değeri atayarak, parolanın doğru olup olmadığını kontrol etmek için bcryptjs karşılaştırma yöntemini bcrypt.compare() kolayca kullanabiliriz.
bcrypt.compare() iki argüman ve bir geri arama alır. Parola, işlev çağrılırken geçirilen paroladır, this.password ise userSchema’dan gelen paroladır.
Kullanıcı nesnesindeki bir özellik gibi olduğundan, kullanıcıların parolasını doğrulamak için bu yöntemi tercih ederim. User.validPassword(password) kolayca çağrılabilir ve yanıt olarak doğru veya yanlış alınabilir.
Mongoose, MongoDB için olduğu gibi, Express de Node içindir.
EJS Şablonlama Motorunu Kullanarak Uygulamamızın Görünümlerini Oluşturma
Uygulamamızın görünümlerini oluşturmaya başlamadan önce, uygulamamızın ön uç mimarisine bir göz atalım.
ÖN UÇ MİMARİSİ / FRONT END MİMARİSİ #
EJS, doğrudan Express ile çalışan bir şablonlama motorudur. Farklı bir ön uç çerçevesine gerek yoktur. EJS, verilerin geçişini çok kolaylaştırır. Ayrıca, arka uçtan ön uca geçiş olmadığından neler olup bittiğini takip etmeyi kolaylaştırır.
Tarayıcıda oluşturulacak dosyaları içerecek bir Views dizinimiz olacak. Tek yapmamız gereken, controllerlarımızda res.render() yöntemini çağırmak.
Örneğin, giriş sayfasını oluşturmak istiyorsak,
res.render(‘login’)kadar basit.
Ayrıca, res.render(‘dashboard’, { user }) gibi render() yöntemine bir nesne olan ek bir argüman ekleyerek görünümlere veri iletebiliriz. Ardından, görüşümüze göre, <%= %> değerlendirme sözdizimi ile verileri görüntüleyebiliriz. Bu etikete sahip her şey değerlendirilir .
Örneğin, <%= user.username %>, kullanıcı nesnesinin username özelliğinin değerini görüntüler. Değerlendirme sözdiziminin yanı sıra, EJS ayrıca koşullu ifadeler, döngüler vb. gibi program kontrol ifadeleri yazmamıza izin veren bir kontrol sözdizimi(<% %>) sağlar.
Temel olarak, EJS, JavaScript’i HTML’mize yerleştirmemize izin verir.
npm install ejs express-ejs-layouts --save
index.js’deekspres-ejs-layoutsgerektirir:
// Add this after the connect-flash import.
const expressLayouts = require('express-ejs-layouts')
// Add this after connecting to MongoDB.
app.use(expressLayouts);
app.set('view engine', 'ejs');
Sonrasında;
cd views
touch layout.ejs
Layout.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.css">
<link rel="stylesheet" href="/static/css/app.css">
<link rel="stylesheet" href="/static/css/intlTelInput.css">
<title>Node js authentication</title>
</head>
<body>
<div class="ui container">
<%- body %>
</div>
<script
src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"
></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/semantic.min.js"></script>
</body>
</html>
EJS de HTML parçalamak ve bu parçaları import etmek
Kısmi kısımlar, yeniden kullanılabilir kodu sakladığımız yerdir, böylece her seferinde yeniden yazmamız gerekmez. Tek yaptığımız, onları ihtiyaç duyulan her yere dahil etmek.
Örneğin, menümüz için kısmi parçalar istiyoruz, böylece sayfamızdaki menüye her ihtiyacımız olduğunda bunun için kod yazmamıza gerek kalmıyor.
cd views
mkdir partials
/views/partials klasöründe iki dosya oluşturacağız:
menu.ejs
<div class="ui secondary menu">
<a class="active item" href="/">
Home
</a>
<% if(locals.user) { %>
<a class="ui item" href="/users/dashboard">
dashboard
</a>
<div class="right menu">
<a class='ui item'>
<%= user.username %>
</a>
<a class="ui item" href="/users/logout">
Logout
</a>
</div>
<% } else {%>
<div class="right menu">
<a class="ui item" href="/users/signup">
Sign Up
</a>
<a class="ui item" href="/users/login">
Login
</a>
</div>
<% } %>
</div>
message.ejs
<% if(typeof errors != 'undefined'){ %> <% errors.forEach(function(error) { %>
<div class="ui warning message">
<i class="close icon"></i>
<div class="header">
User registration unsuccessful
</div>
<%= error.msg %>
</div>
<% }); %> <% } %> <% if(success_msg != ''){ %>
<div class="ui success message">
<i class="close icon"></i>
<div class="header">
Your user registration was successful.
</div>
<%= success_msg %>
</div>
<% } %> <% if(error_msg != ''){ %>
<div class="ui warning message">
<i class="close icon"></i>
<div class="header">
</div>
<%= error_msg %>
</div>
<% } %> <% if(error != ''){ %>
<div class="ui warning message">
<i class="close icon"></i>
<div class="header">
</div>
<%= error %>
</div>
<% } %>
DASHBOARD SAYFASINI OLUŞTURMA
Views klasörümüzde bir dashboard.ejs dosyası oluşturuyoruz:
<%- include('./partials/menu') %>
<h1>
DashBoard
</h1>
Burada menü bölümlerini ekliyoruz, böylece menü sayfaya eklenmiş olacak.
HATA SAYFASI OLUŞTURMA
Views klasörümüzde bir error.ejs dosyası oluşturuyoruz:
<h1>Error Page</h1>
<p><%= error %></p>
BUILDING THE HOME PAGE
Views klasörümüzde bir home.ejs dosyası oluşturuyoruz:
<%- include('./partials/menu') %>
<h1>
Welcome to the Home Page
</h1>
GİRİŞ SAYFASINI OLUŞTURMA
Views klasörümüzde bir login.ejs dosyası oluşturuyoruz:
<div class="ui very padded text container segment">
<%- include ('./partials/message') %>
<h3>
Login Form
</h3>
<form class="ui form" action="/users/login" method="POST">
<div class="field">
<label>Email</label>
<input type="email" name="email" placeholder="Email address">
</div>
<div class="field">
<label>Password</label>
<input type="password" name="password" placeholder="Password">
</div>
<button class="ui button" type="submit">Login</button>
</form>
</div>
DOĞRULAMA SAYFASINI OLUŞTURMA
Views klasörümüzde bir verify.ejs dosyası oluşturuyoruz:
<%- include ('./partials/message') %>
<h1>Verify page</h1>
<p>please verify your account</p>
<form class="ui form" action="/users/verify" method="POST">
<div class="field">
<label>verification code</label>
<input type="text" type="number" name="verifyCode" placeholder="code">
</div>
<button class="ui button" type="submit">Verify</button>
</form>
<br>
<a class="ui button" href="/users/resend">Resend Code</a>
Burada, kullanıcıların kendilerine gönderilecek doğrulama kodunu girmeleri için bir form sağlıyoruz.
KAYIT SAYFASI OLUŞTURMA
Kullanıcının cep telefonu numarasını almamız gerekiyor ve hepimiz biliyoruz ki ülke kodları ülkeden ülkeye farklılık gösteriyor.
Bu nedenle, ülke kodları ve telefon numaralarının doğrulanması konusunda bize yardımcı olması için intl-tel-input kullanacağız.
npm install intl-tel-input
- Public klasörümüzde bir css dizini, js dizini ve img dizini oluşturuyoruz:
cd public
mkdir css js img
2. intlTelInput.css dosyasını node_modules\intl-tel-input\build\css\ dosyasından public/css dizinimize kopyalıyoruz.
3. node_modules\intl-tel-input\build\js\ klasöründen hem intlTelInput.js hem de utils.js dosyasını public/js dizinimize kopyalıyoruz.
4. node_modules\intl-tel-input\build\img\ klasöründeki flags.png ve [email protected]’yi public/img dizinimize kopyalıyoruz.
public/css klasörümüzde bir app.css oluşturuyoruz:
.iti__flag {background-image: url("/static/img/flags.png");}
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
.iti__flag {background-image: url("/static/img/[email protected]");}
}
.hide {
display: none
}
.error {
color: red;
outline: 1px solid red;
}
.success{
color: green;
}
Son olarak, görünümler klasörümüzde bir signup.ejs dosyası oluşturuyoruz:
<div class="ui very padded text container segment">
<%- include ('./partials/message') %>
<h3>
Signup Form
</h3>
<form class="ui form" action="/users/signup" method="POST">
<div class="field">
<label>Name</label>
<input type="text" name="name" placeholder="name">
</div>
<div class="field">
<label>Username</label>
<input type="text" name="username" placeholder="username">
</div>
<div class="field">
<label>Password</label>
<input type="password" name="password" placeholder="Password">
</div>
<div class="field">
<label>Phone number</label>
<input type="tel" id='phone'>
<span id="valid-msg" class="hide success">✓ Valid</span>
<span id="error-msg" class="hide error"></span>
</div>
<div class="field">
<label>Email</label>
<input type="email" name="email" placeholder="Email address">
</div>
<button class="ui button" type="submit">Sign up</button>
</form>
</div>
<script src="/static/js/intlTelInput.js"></script>
<script>
const input = document.querySelector("#phone")
const errorMsg = document.querySelector("#error-msg")
const validMsg = document.querySelector("#valid-msg")
const errorMap = ["Invalid number", "Invalid country code", "Too short", "Too long", "Invalid number"];
const iti = window.intlTelInput(input, {
separateDialCode: true,
autoPlaceholder: "aggressive",
hiddenInput: "phonenumber",
utilsScript: "/static/js/utils.js?1590403638580" // just for formatting/placeholders etc
});
var reset = function() {
input.classList.remove("error");
errorMsg.innerHTML = "";
errorMsg.classList.add("hide");
validMsg.classList.add("hide");
};
// on blur: validate
input.addEventListener('blur', function() {
reset();
if (input.value.trim()) {
if (iti.isValidNumber()) {
validMsg.classList.remove("hide");
} else {
input.classList.add("error");
var errorCode = iti.getValidationError();
errorMsg.innerHTML = errorMap[errorCode];
errorMsg.classList.remove("hide");
}
}
});
// on keyup / change flag: reset
input.addEventListener('change', reset);
input.addEventListener('keyup', reset);
document.querySelector('.ui.form').addEventListener('submit', (e) => {
if(!iti.isValidNumber()){
e.preventDefault()
}
})
</script>
Pasaportla Temel Kimlik Doğrulama
Bir uygulamaya kimlik doğrulaması eklemek gerçekten karmaşık ve zaman alıcı olabilir, bu yüzden bize bu konuda yardımcı olacak bir pakete ihtiyacımız var.
Unutmayın: Uygulamanızın özel bir ihtiyacı olmadıkça, tekerleği yeniden icat etmeyin.
pasaport, ekspres uygulamamızda kimlik doğrulamaya yardımcı olan bir pakettir.
pasaportun kullanabileceğimiz birçok stratejisi var, ancak local-strategy’i kullanacağız ve bu temel olarak kullanıcı adı ve şifre doğrulaması yapar.
500 den fazla şifre stratejilerinin olduğu passportjs kütüphanesinden local-passport u kullanmayı tercih ettim. Dilerseniz diğer yöntemlerede göz atabilirsiniz. https://www.passportjs.org/
npm install passport passport-local
index.js’de aşağıdaki kodu ekliyoruz:
// Add this after importing Express.
const passport = require('passport')
// Add this after importing Mongoose.
const { localAuth } = require('./config/passportLogic')
// Add this after creating the Express app.
localAuth(passport)
// Add this after the URL-encoded body parser.
app.use(passport.initialize());
app.use(passport.session());
index.js dosyamıza bazı uygulama düzeyinde ara katman yazılımları ekliyoruz — bu, uygulamaya pasaport.initialize() ve pasaport.session() ara katman yazılımını kullanmasını söyler.
Passport.initialize() pasaportu başlatırken, pasaport.session() ara yazılımı pasaportun kimlik doğrulama için oturum kullandığımızı bilmesini sağlar.
localAuth()işlevi hakkında fazla endişelenmeyin. Bu, pasaport nesnesini bir argüman olarak alır ve hemen aşağıdaki işlevi yaratacağız.
Bir yapılandırma klasörü oluşturuyoruz ve gerekli dosyaları oluşturuyoruz:
mkdir config
touch passportLogic.js middleware.js
passportLogic.js
//file contains passport logic for local login
const LocalStrategy = require('passport-local').Strategy;
const mongoose = require('mongoose')
const User = require('../models/user')
const localAuth = (passport) => {
passport.use(
new LocalStrategy(
{ usernameField: 'email' }, async(email, password, done) => {
try {
const user = await User.findOne({ email: email })
if (!user) {
return done(null, false, { message: 'Incorrect email' });
}
//validate password
const valid = await user.validPassword(password)
if (!valid) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
} catch (error) {
return done(error)
}
}
));
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
}
module.exports = {
localAuth
}
Yukarıdaki kodda neler olduğunu anlayalım.
Require deyimlerinin yanı sıra, dosyadan dışa aktarılacaklocalAuth() işlevini oluşturuyoruz. İşlevde, kullanıcı adı ve parola tabanlı kimlik doğrulama için LocalStrategy()’yi kullanan pasaport.use()işlevini çağırıyoruz.
usernameField’imizin e-posta olması gerektiğini belirtiyoruz. Ardından, belirli bir e-postaya sahip bir kullanıcı bulmalıyız eğer yoksa, done() işlevinde bir hata döndürürüz. Ancak bir kullanıcı varsa,
User objesi üzerinde validPassword methodunu kullanarak parolanın geçerli olup olmadığını kontrol ederiz. Tabi geçersizse, bir hata döndürürüz.
Son olarak, her şey başarılı olursa, kullanıcıyı done(null, user) olarak döndürmeliyiz.
pasaport.serializeUser() ve pasaport.deserializeUser(), oturum açma session’larını desteklemek için yardımcı olur. Passport, oturuma giden ve oturumdan kullanıcı örneklerini serialize eder.
Middleware.js
//check if a user is verified
const isLoggedIn = async(req, res, next) => {
if(req.user){
return next()
} else {
req.flash(
'error_msg',
'You must be logged in to do that'
)
res.redirect('/users/login')
}
}
const notLoggedIn = async(req, res, next) => {
if(!req.user) {
return next()
} else{
res.redirect('back')
}
}
module.exports = {
isLoggedIn,
notLoggedIn
}
Ara katman yazılımı dosyamız, daha sonra rotalarımızda kullanılacak olan iki (2) rota düzeyinde ara katman yazılımı içerir.
Rota düzeyinde ara katman yazılımı, rotalarımız tarafından çoğunlukla yetkilendirme gibi rota koruma ve doğrulama için kullanılırken, uygulama düzeyinde ara katman tüm uygulama tarafından kullanılır.
isLoggedIn ve notLoggedIn, bir kullanıcının oturum açıp açmadığını kontrol eden yol düzeyinde ara yazılımlardır. Bu ara katman yazılımlarını, oturum açmış kullanıcılar için erişilebilir kılmak istediğimiz rotalara erişimi engellemek için kullanırız.
SIGN-UP CONTROLLER’IN OLUŞTURULMASI
cd controllers
mkdir signUpController.js loginController.js
signUpController.js
Bu controller yaptığı görevler:
- Kullanıcıların kimlik bilgilerini kontrol etme;
- Veritabanımızda bu ayrıntıya (e-posta veya telefon numarası) sahip bir kullanıcının olup olmadığını kontrol etme;
- Kullanıcı varsa bir hata oluşturma;
- Son olarak böyle bir kullanıcı yoksa verilen detaylar ile yeni bir kullanıcı oluşturup login sayfasına yönlendirme.
//signUpController.js
const mongoose = require('mongoose')
const User = require('../models/user')
//sign up Logic
const getSignup = async(req, res, next) => {
res.render('signup')
}
const createUser = async (req, res, next) => {
try {
const { name, username, password, phonenumber, email} = await req.body
const errors = []
const reRenderSignup = (req, res, next) => {
console.log(errors)
res.render('signup', {
errors,
username,
name,
phonenumber,
email
})
}
if( !name || !username || !password || !phonenumber || !email ) {
errors.push({ msg: 'please fill out all fields appropriately' })
reRenderSignup(req, res, next)
} else {
const existingUser = await User.findOne().or([{ email: email}, { phonenumber : phonenumber }])
if(existingUser) {
errors.push({ msg: 'User already exists, try changing your email or phone number' })
reRenderSignup(req, res, next)
} else {
const user = await User.create(
req.body
)
req.flash(
'success_msg',
'You are now registered and can log in'
);
res.redirect('/users/login')
}
}
} catch (error) {
next(error)
}
}
module.exports = {
createUser,
getSignup
}
loginController.js
Bu controllerin görevleri:
- Kullanıcının var olup olmadığını kontrol etmek için yerel kapsam (local scope ) yani e-posta ve şifre ile
pasaport.authenticate()yöntemini kullanır - Kullanıcı yoksa hata mesajı veriyoruz ve kullanıcıyı aynı rotaya yönlendirir;
- Kullanıcı varsa,
req.logInyöntemini kullanarak kullanıcıya giriş yaptırır. sendVerification()işlevini kullanarak bir doğrulama gönderir ve ardından onları doğrulama yoluna yönlendirir.
const mongoose = require('mongoose')
const passport = require('passport')
const User = require('../models/user')
const { sendVerification } = require('../config/twilioLogic')
const getLogin = async(req, res) => {
res.render('login')
}
const authUser = async(req, res, next) => {
try {
passport.authenticate('local', function(err, user, info) {
if (err) {
return next(err)
}
if (!user) {
req.flash(
'error_msg',
info.message
)
return res.redirect('/users/login')
}
req.logIn(user, function(err) {
if (err) {
return next(err)
}
sendVerification(req, res, req.user.phonenumber)
res.redirect('/users/verify');
});
})(req, res, next);
} catch (error) {
next(error)
}
}
module.exports = {
getLogin,
authUser
}
Şu anda sendVerification() tam olarak çalışmıyor. Bunun nedeni, işlevi yazmamış olmamızdır, bunun için Twilio’ya ihtiyacımız var. Twilio’yu yükleyelim ve başlayalım.
Rotaları Korumak İçin Twilio Verify’ı Kullanma
-
https://www.twilio.com/ adresine gidin;
-
Twilio ile bir hesap oluşturun;
-
Kontrol panelinize giriş yapın;
-
Yeni bir proje oluştur’u seçin;
-
Yeni bir proje oluşturmak için adımları izleyin.
node.js için Twilio SDK’yı kurmak için:
npm install twilio
Ardından, ortam değişkenleri konusunda bize yardımcı olması için dotenv’i kurmamız gerekiyor.
npm install dotenv
Projemizin kök dizininde bir dosya oluşturuyoruz ve adını .env olarak veriyoruz. Bu dosya, kimlik bilgilerimizi sakladığımız yerdir, bu yüzden onu git’e repomuza koymayız. Bunu yapmak için projemizin kök dizininde bir .gitignore dosyası oluşturuyoruz ve dosyaya aşağıdaki satırları ekliyoruz:
node_modules
.env
Bu, git’e hem node_modules klasörünü hem de .env dosyasını yoksaymasını söyler.
.env
TWILIO_ACCOUNT_SID=YOUR_ACCOUNT_SID
TWILIO_AUTH_TOKEN=YOUR_AUTH_TOKEN
TWILIO_VERIFICATION_SID=YOUR_VERIFY_SERVICE_SID
SESSION_SECRET=YOUR_RANDOM_SESSION_SECRET
Bu değişkenleri yerel .env dosyana ekle ve dosyayı repoya koyma. Verify servisini oluşturduktan sonra TWILIO_VERIFICATION_SID değerini doldur. Oturum anahtarı için rastgele bir değer kullan.
twilioLogic.js
require('dotenv').config()
const twilio = require('twilio')
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)
//create verification service
const createService = async(req, res) => {
client.verify.services.create({ friendlyName: 'phoneVerification' })
.then(service => console.log(service.sid))
}
createService();
Yukarıdaki kod parçasında yeni bir doğrulama hizmeti oluşturuyoruz.
node config/twilioLogic.js
config/twilioLogic.js’de, doğrulama hizmetini yalnızca bir kez oluşturmamız gerektiğinden createService() satırını kaldırıyoruz. Ardından aşağıdaki kod satırlarını ekliyoruz:
// Create the Verify service once, then remove the createService call.
//send verification code token
const sendVerification = async(req, res, number) => {
client.verify.services(process.env.TWILIO_VERIFICATION_SID)
.verifications
.create({to: `${number}`, channel: 'sms'})
.then( verification =>
console.log(verification.status)
);
}
//check verification token
const checkVerification = async(req, res, number, code) => {
return new Promise((resolve, reject) => {
client.verify.services(process.env.TWILIO_VERIFICATION_SID)
.verificationChecks
.create({to: `${number}`, code: `${code}`})
.then(verification_check => {
resolve(verification_check.status)
});
})
}
module.exports = {
sendVerification,
checkVerification
}
sendVerification, sms kanalı kullanılarak sağlanan numaraya bir doğrulama OTP’si gönderen bir söz veren eşzamansız bir işlevdir.
checkVerification ayrıca, doğrulamanın durumunu kontrol eden bir söz veren eşzamansız bir işlevdir. Kullanıcılar tarafından sağlanan OTP’nin kendilerine gönderilen OTP ile aynı olup olmadığını kontrol eder.
config/middleware.js’de aşağıdakileri ekleyin:
// Add this after notLoggedIn.
//prevents an unverified user from accessing '/dashboard'
const isVerified = async(req, res, next) => {
if(req.session.verified){
return next()
} else {
req.flash(
'error_msg',
'You must be verified to do that'
)
res.redirect('/users/login')
}
}
//prevent verified User from accessing '/verify'
const notVerified = async(req, res, next) => {
if(!req.session.verified){
return next()
} else {
res.redirect('back')
}
}
module.exports = {
// Keep the existing notLoggedIn export.
isVerified,
notVerified
}
Rotalarımızda kullanılacak iki rota düzeyinde ara katman yazılımı daha oluşturduk.
isVerified ve notVerified bir kullanıcının doğrulanıp doğrulanmadığını kontrol eder. Bu ara yazılımları, yalnızca doğrulanmış kullanıcılar tarafından erişilebilir kılmak istediğimiz rotalara erişimi engellemek için kullanıyoruz.
verifyController.js
const mongoose = require('mongoose')
const passport = require('passport')
const User = require('../models/user')
const { sendVerification, checkVerification } = require('../config/twilioLogic')
const loadVerify = async(req, res) => {
res.render('verify')
}
const resendCode = async(req, res) => {
sendVerification(req, res, req.user.phonenumber)
res.redirect('/users/verify')
}
const verifyUser = async(req, res) => {
//check verification code from user input
const verifyStatus = await checkVerification(req, res, req.user.phonenumber, req.body.verifyCode)
if(verifyStatus === 'approved') {
req.session.verified = true
res.redirect('/users/dashboard')
} else {
req.session.verified = false
req.flash(
'error_msg',
'wrong verification code'
)
res.redirect('/users/verify')
}
}
module.exports = {
loadVerify,
verifyUser,
resendCode
}
resendCode() doğrulama kodunu kullanıcıya yeniden gönderir.
verifyUser, önceki bölümde oluşturulan checkVerification işlevini kullanır. Durum onaylanırsa, req.session’daki doğrulanmış değeri true olarak ayarlarız.
Kullanıcı Rotalarını Oluşturma
Temel olarak, uygulamamız aşağıdaki rotalara sahip olacak:
/user/login: kullanıcının giriş yapması için;/user/signup: kullanıcının üye olması için;/user/logout: kullanıcının çıkış yapması için;/user/resend: kullanıcının doğrulama kodunu tekrar göndermesi için;/user/verify: kullanıcı kodunun girilmesi için;/user/dashboard: Twilio Verify kullanılarak korunan rota
cd routes
touch user.js
routes/user.js’de gerekli paketleri gerektirir:
const express = require('express')
const router = express.Router()
const { createUser, getSignup } = require('../controllers/signUpController')
const { authUser, getLogin } = require('../controllers/loginController')
const { loadVerify, verifyUser, resendCode } = require('../controllers/verifyController')
const { isLoggedIn, isVerified, notVerified, notLoggedIn } = require('../config/middleware')
//login route
router.route('/login')
.all(notLoggedIn)
.get(getLogin)
.post(authUser)
//signup route
router.route('/signup')
.all(notLoggedIn)
.get(getSignup)
.post(createUser)
//logout
router.route('/logout')
.get(async (req, res) => {
req.logout();
res.redirect('/');
})
router.route('/resend')
.all(isLoggedIn, notVerified)
.get(resendCode)
//verify route
router.route('/verify')
.all(isLoggedIn, notVerified)
.get(loadVerify)
.post(verifyUser)
//dashboard
router.route('/dashboard')
.all(isLoggedIn, isVerified)
.get(async (req, res) => {
res.render('dashboard')
})
//export router
module.exports = router
Yukarıdaki kod parçasında rotalarımızı oluşturuyoruz, bakalım burada neler oluyor:
router.route() rotayı belirtir.
router.route(‘/login’)belirtirsek, login route’unu hedeflemiş oluruz.
.all([middleware]), bu rotaya yönelik tüm isteklerin bu ara yazılımı kullanması gerektiğini belirtmemize olanak tanır.
router.route('/login')
.all([middleware])
.get(getController)
.post(postController)
sözdizimi çoğu geliştiricinin alışık olduğu sözdizimine bir alternatiftir.
router.get('/login', [middleware], getController);
router.post('/login', [middleware], postController);
ile aynı şeyi yapar.
Şimdi terminalimize aşağıdaki komutu yazarak uygulamamızı çalıştırırsak:
npm run dev
full-stack express uygulamamız çalışır durumda olmalıdır.
Projeye github reposu üzerinden ulaşabilirsiniz. https://github.com/fatih255/Twilo-OTP-entegration-with-express-JS