عن الString based Token- stateful Authentication
Momen Zalabany
Posted on April 29, 2021
Part I: Introduction to Authentication
Part II: String-Based-Token-Authentication [you are reading it]
Part III: Token-Based (JWT) Authentication [soon..]
ده الجزء الثاني ( يريت لو مقريتش المقدمه في الجزء الاول تاخد فكره عنه من هنا : )
الناهرده حنروح رحله نتخيل فيها من اول انك سمعت عن التوكنز و قررت تحمي البلاتفرم بتاعك بيها بدل الsession < حنمشي من أول most naive approach لحد مانوصل انشاء الله اننا طبقنا الستنادرد صح.
انت عندك single page application & api backend و اكتشفت ان السشنز مش حتنفع لان الباك اند مش علي نفس السب دمين مثلا فامينفعش نستخدم كوكيز بسهله: او انك شغال علي microservices و محتاج statless auth .. أو أي سبب من الجزء الاول.
المحاوله الاوله: STATEFUL STRING BASED AUTH
البروسس اللي جت على بالك كانت كالاتي :
- client send username & password
- backend validate; then generate MD5 Random Hash as a token
- save token into the database
- set HTTP client to send Authentication header that includes token on every request;
- API will try to find userinfo by searching for token
-pseudo code using HTTP client Axios on frontend and ExpressJS for API-
//client side:
axios.post('/token',{username,password})
.then(user => setAuthHeaderFromUser(user) )
.then(user => saveUserToStorage(user) )
// server side
route.post('/token', (req, res, next) => {
// ... validate username and password
getUserByCredentials(req.body))
.catch(()=>next(new Error('username or password is wrong'))
.then(generateToken)
.then(r=>res.json(r))
});
// generateToken example
const generateToken = async user =>{
let isUnique= false;
let auth_token = null;
// make sure token is unique
while( isUnique === false ) {
auth_token = md5( user.id + user.salt + Math.random())
isUnique = (await db.query(`select 1 from users where token=$1`, auth_token)).row_count === 0
}
// save new token to DB and return user object
db.query(update users set token = $1 where users.id=$2, auth_token, user.id)
return {user, auth_token}
}
آخر خطوة إننا نحمي البلاتفرم بتاعنا نمنع اي حد معوش توكن انه يوصل: هنا عندك طريقتين :
- authentication as a filter: like in image attached, you have a separate layer before your router that block un-authenticated requests
- individual route auth: you let every route handle auth on its own. // example middleware you can add in Express before loading routes
// if you want to use (filter approach) ; or in route chain
function authMiddleware(req, res, next){
const token = `${req.header['Authentication']}`.split(' ').pop();
if(!token){
return next(new Error('access denied'))
}
db.query('select * from users where token=$1',token)
.then(user=> req.locals.USER = user)
.then(next)
}
تمامز كده , عملت push و فرحان هوب تاني يوم الكلينت بيكلمك "اليوزر لو عمل لوجين من جهاز بيطلع من التاني .. بترود عليه بثقة
"its not a bug, its a feature" 😃 !!
ترجع تبص على الكود, فتلاقي إن الراجل عنده حق, احنا فوق حطينا column في اليوزر table فـ كل مالجين التوكن بتتغير token column over-written فااليوزر القديم بيطلع بره لإن التوكن بتاعته اتمسحت >>
الحمد لله الحل سهل "normalize users table, move token to its own table " one to many relation
عملت push تاني و حسست العميله انه هوه الخسران دي كانت سيكيورتي فيتشر ! 😛 , و تعدي الايام و يجيلك تليفون :
"الاكونت بتاعي التسرق, و مش عارف احمي نفسي, دانا حتي غيرت الباسورد و الهاكر لسه بيدخل"
فكرت شويه - مهو مشحينفع نقوله دي فيتشر المره دي !-
بتفتح الموقع و قررت تعمل سيكيورتي رفيو: اكتشفت حاجه قاتله فالعك لي احتا عاملينه فوق:
- التوكن ملعاش تاريخ انتهاء, يعني لو التوكن اتسرقت بخ كده.
- الموقع شغال علي HTTP يعني الكونكشن مش متشفره, اي هاكر مبتدا ممكن يعمل "Man in Middle " و يتصنت علي الريكوستات و يقرا التوكن من غير مايجي جمبك !
- اليوزر لو قعد يعمل لوجن DDOS ممكن يعمل ملاين التوكنز و يبطء الموثع كله (متنساش انك اخترت stateful approach , يعني كل ريكوست بيجيلك, لازم بتكلم الداتابيز و لو عندك ملاين التوكنز في داتابيز مش اوبتميزد او عليها proper indexs كده كل اليوزرز حيبطءو مش بس الي بيحاول ي دي دوسك 🙂
- اليوزر معندوش القدره انه يمسح كل التوكنز بتاعته (logout from all devices)
ترد علي الكلينت بكل ثقه: مهو انا قولت لك ان كان في سيكيورتي فيتشر و انت الي قولت مش عايزها 😛 >> طبعا الكلينت او التيم ليد مش حيخش عليه شغل الدفيلوبرز ده و حيديك علي وشك بس اهي محاوله اننا نشتري وقت لحد منشوف المصيبه دي...
نمسك مشكله مشكله و تحلها.
الHTTP دي جريمة للأسف لسة في موقع بنشوفها -خصوصا في السترتب - استخدام https هي حاجة لاغنى عنها في 2020 خصوصا مع وجود منظمه ذي let's-encrypt / certbot الي بيدو سيرتفكت بلمجان . فامش حتكلم عنها.. بس افتكر دايما اي حد يستخدم auth in http يبقي لامؤخده.
حمايه التوكن:
set an expiry date for all tokens
bind auth-token to IP address that created it
create a one-time-usage token (AKA. Refresh-Token)
add mechanism to de-validate all tokens -either delete all or use salt that user can request to change it-
بس قبل ما اتكلم حابب اشير الي ان فكره ان التوكن تتسرق, دي حاجه مش سهله, يعني لو الموقع بتاعك مفيهوش تغرات مثل (XSS) , و البروزر بتاع العميل معليهوش بلجن حرامي ::]
التوكن بتبقي متسجله في البروزر بامان و الاتصال مبين الموقع بتاعك و العميل متشفره بلHTTPS .. يعني الموضوع مخيف اكيد , و ماذال احتمال سرقه التوكن عن طريق physical access او phishing attack p حد يقول لليوزر افتح الكونسول و اكتب كذا و يسرق منه التوكن بهبله ماذالت موجوده.. ده غير ان الوقايه خير من العلاج مدام الاحتماليه موجوده و الحل كمان موجود فامعلش لازم تتعب معانا شويه..
اولا: تحديد تاريخ صلاحيه التوكن:
يمكن من اهم و اصعب خطوط الدفاع, الفكره كلها انك تخلي التوكن بتنتهي بعد 10-15 دقيقه بحيث ان لو اتسرقت, احتمال كبير تنتهي قبل ما الهاكر يستخدمها. -ده غير ان ميصحش توكن اتعملت سنه 2014 تفضل شغاله ابد الدهر-
عيب تحديد الصلاحيه: انت كده اليوزر حايلوجن, بعد 15 دقيقه و هوه شغال في نص شغاله بيدوس save مثلا يلاقي نفسه بره >> ولازم يلوجين تاني.. ==ممكن تكون أخت بالك فرمضان في موقع مشور جدا وللأسف كل ,تفتحه يقولك لوجين تاني *بدون ذكر اسماء بقى - :evil في الوقت الي netflix مثلا من ساعت معملت الاكونت و هوه سكنيه في الحلاوه اول ماتفتح تلاقيه فاكرك==
الحل: بدل ماتعمل توكن واحده: اعمل اتنين , واحده عمرها قصير و تستخدم في كل حاجه -auth-token- , و واحده تانيه عمرها طويل و تستخدم لاستخراج auth-token جيده فقط وتنتهي بمجرد استخدامها -refresh token- اي محاوله لاستخدام الريفرش توكن مرتين يعتبر محاوله اختراق و يجب اتباع بروسس لتحذير المستخدم و الغاء صلاحيه جميع التوكنز الخاصه بيه و تنبيهه انه تم اختراق حسابه- جنتكلم باستفاضه عن الريفرش بعد شويه.
// psudo-code modify generateToken function
const generateToken = async user =>{
..same as old code
// save new token to DB and return user object
auth_token= generateRadomMD5();
refresh_token= md5(auth_token + Math.random())
db.query('insert into tokens(ip, token, type , createdAt, used) values($1,$2,$3, 0);', req.ip, auth_token, 'auth', new Date(),0);
db.query('insert into tokens(ip, token, type , createdAt, used) values($1,$2,$3, 0);', req.ip, refresh_token, 'refresh', new Date(), 0);
return {user, refresh_token, auth_token}
we ba3deen ne-modify el authMiddleware ele fo2 3ashn ye7terem el expiry date
function authMiddleware(req, res, next){
const token = `${req.header['Authentication']}`.split(' ').pop();
if(!token){
return next(new AuthError(401, 'access denied'))
}
db.query('select *.users from tokens where token=$1 and tokens.createdAt < DATE_SUB(NOW(),INTERVAL 15 MINUTE) left join users on users.id = tokens.uid',token)
.then(user=> req.locals.USER = user)
.then(next) // all good continue by calling next()
.catch(()=> next( new AuthError(403, 'token expired or does not exsist'))
}
تاخد بالنا ##هنا من الhttp status code :
403 = Forbidden و بنستخدمها عشان نقول لليوزر ( تمام انت بعت توكن بس هيه مش سليمه, حاول تاني)
401= Unauthorized , نستخدمها عشان نقول لليوزر لا انت مينفعش تحاول تاني غير لمه تعمل لوجن تاني و تبعت توكن مع الريكوست (اعاده المحاوله مش متوقعه من غير متجيب توكن)
403 هنا بتقول للكلينت ان ممكن التوكن تكون اتمسحت, او عدي عليها اكتو م 15 دقيقه , فايريت تستخدم اللريفرش توكن عشان تجدد صلاحيه التوكن بتاعتك
ده غير ان في كولم جديد بنسجل فيه اذا كانت الريفرش توكن دي استخدمت وال لا.
اخيرا نعمل new route ت رفرش token
app.post('/token', // same as above);
app.get('/token', function (req, res, next){
const refresh_token = req.body.refresh_token;
db.query('select used from tokens where type='refresh' and token=$1', refresh_token, req.locals.USER.id)
.then(tokenRow =>
!token
? Promise.reject('NOT_FOUND')
: token.used !== 0
? Promise.reject('ALREADY_USED')
: req.locals.USER
)
.then(generateToken)
.then(newTokens=> res.status(201).json(newTokens) )
.catch(e=> e==='NOT_FOUND' ? next(401) : next( LogHacking Attempt() );
}
تمام كده السيرفر جاهز , فاضل الفرونتند : الفكرة كلها إننا نسمع لكل el-XHR requests وأول مانلاقي اررور نشوف اذا كان ده 403 يبقى نحاول ne-refresh ال token .. على حسب إنت بتستخدم انهي fetch wrapper ممكن تنفيذ ده ببساطة بس لازم تاخد بالك من حاجة .. إن لو كذا ريكويست طلعوا في نفس الوقت ماتحاولش ترفرش التوكن مرتين والى السيرفر حيفتكر إن التوكن اتسرقت وفي اكتر من شخص بيحاولوا يعملوا refresh ..
example if you use custom fetch Wrapper
const AppFetch = (url, config) => {
return fetch(url, config)
.catch(e=>{ // here is retry on failure
return (e.status === 401) ?
refreshToken().then(()=> AppFetch(url, config))
: Promise.reject(e)
})
}
or if you use Axios, it already comes with interceptors:-
axios.interceptors.response.use(function (response) {
// Any status code that within the range of 2xx will trigger
return response;
}, function (error) {
// Any status codes that falls outside the range of 2xx trigger
return (e.status === 401)
? refreshToken().then(()=> axios(config))
: Promise.reject(e)
});
keda only one thing remaining, to make sure we don't try to refresh token again if a (refreshToken()) is already in progress.
modify above code with:
const promiseRef = {current: null};
... rest...
return (e.status === 401) ? refreshToken(promiseRef).then(()=> axios(config)
: Promise.reject(e)
/// funciton refreshToken(ref){
if(!ref.current) // mutate object with refresh promise
ref.current = axios.get('/token',{params: localStorage.refreshToken})
.then(newTokens=>{
localStorage.set('refreshToken', newTokens.refresh_token)
axios.defaults.headers.common['Authorization'] =
newTokens.auth_token;
ref.current = null // clear promise ref;
return newTokens;
}).catch(e=>{ ref.current = null; return e; }
return ref.current // return refresh promise so only one rrefresh request will ever start no matter how many times it was called
}
بس كده اول ما التوكن تاكسبير حتريفرش نفسها و لو كذا ريكوست طلعو فس نفس الوقت, حيستحدموا نفس الPromise و بعد ما يخاص حيعيدو المحاوله..
طبعا مش محتاج اقول ان الكود الي فوق مجرد مثال, و مكتوب في الفاسبوك فا اكيد فيه اخطاء المهم عندي ان الفكره توصل للجميع.. الimplementation كل واحد و زوقه 🙂
فاضل كده اللوج اوت, و ده في الSTATLFULL سهل , اعتقد مش محتاج شرح , يا تمسح كل التوكنس من الداتباس أو تستخدم SALT وتعيرو لما اليوزر يحب يطلع من كل الأجهزة ..
انا اسف البوست طول جدا المره دي, المره القادمه نتكلم بقي عن الstateless Token based and its implementation for micro-services
Posted on April 29, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024