احراز هویت با استفاده از github در express

در این پست میخوام authentication رو با استفاده از github در backend پیاده سازی کنم. برای این موضوع بهتره کمی عقب تر بریم و درک عمیق تری از شیوه ی authentication داشته باشیم.

ما در اینجا میخواهیم از oauth2 استفاده کنیم که یک استاندارد برای احراز هویت کاربران برای دسترسی به منابع یا داده ها در api است.

در RFC اون شکل زیر اومده که شاید خوندنش کمی پیچیده باشه اما به زبان ساده تر و خلاصه:

استراتژی این استاندارد اینطوریه که برای جلوگیری از دسترسی مخرب به داده یا منبع، باید ابتدا یک درخواست با نام کاربری و رمز عبور برای احراز هویت به سرویس دهنده فرستاده شود و برای این درخواست clientID ای مشخص به اون درخواست رو دریافت کرد و در ازای ارسال این clientID به سرویس دهنده یک رشته ( string ) رمزگذاری شده بعنوان توکن (token) ساخت و برگردانده شود که حالا این توکن نشان اجازه دسترسی ما به داده یا منبع مورد نظر خواهد شد چرا که هم client و هم server از اون خبر دارند.

ما میتونیم مراحل رو خودمون پیاده سازی کنیم یعنی یه رشته منحصر بفرد بر اساس clientID برای نام کاربری درخواست داده شده ایجاد کنیم و عملیات بالا رو برای احراز هویت کاربر انجام بدیم اما از اونجایی که سایتهایی مثل گیت هاب و توییتر و فیسبوک و گوگل و ... [که اینجا به اونها provider میگیم ] هم از این روش (oauth) استفاده میکنند و ما به اونها اعتماد داریم میتونیم این مراحل رو به اونا بسپریم و تنها کاری که انجام باید بدهیم این است که کاربر را به سمت انها بفرستیم و انتظار داشته باشیم توکن منحصر بفرد تولید شده را از اون provider دریافت و ذخیره کنیم و در هر درخواست از مقدار توکن، کاربر متناظرش رو تشخیص بدهیم.

خب حالا که کمی با تئوری ماجرای oauth authentication آشنا شدیم بریم سراغ پیاده سازی:

من برای پیاده سازی از passportjs استفاده کردم که شاید کامل ترین پیکیجی بود که دیدم.

بعد از نصب باید بخاطر داشته باشید که باید با استفاده از passport.initialize به برنامه خودتون بفهمونید که از این پکیج بعنوان middleware استفاده کنه اما حواستون باشه که ما اینجا نمیخوایم از session استفاده کنیم پس از passport.session استفاده نخواهیم کرد.

اما چرا از session استفاده نکردم؟ تصور کنید توکن کاربر احراز هویت شده رو دارید. با اون چه باید بکنید تا در درخواست های بعدی این اطلاعات از دست نره و مجبور نباشید هر بار احراز هویت کنید. درسته که میتونیم این اطلاعات رو در session ذخیره کنیم اما تصور کنید میخواهید با استفاده از توکن بدست آمده هم روی مرورگر و هم روی یک اپلیکشین موبایل لاگین کنید. اگر نمیخواهید کاربر مجبور به احراز هویت مجدد شود باید توکن رو به درخواست دهنده بدهید تا در هر کجا که خواست از اون استفاده کنه و برای شما این مهم باشه که این توکن در دیتابیس شما اعتبار دارد و یا خیر. البته اشکال این کار اینه که اگر توکن به دست هرکسی بیفته میتونه با اون درخواست بده ولی باید ببینید کدوم برای شما اولویت داره

پکیج passport دارای استراتژی های مختلف هستش که ما میخواهیم از گیت هاب استفاده کنیم. پس از اینجا اون استراتژی رو به پروژه تون اضافه کنید. حالا میتونید از اون استراتژی در برنامه تون استفاده کنید اما چطوری؟

const passport = require('passport');
​
var GitHubStrategy = require('passport-github2').Strategy;
​
passport.use(new GitHubStrategy({
    clientID: 'clientIDFromGithub',
    clientSecret: 'clientSecretFromGithub',
    scope: ['user:email'],
    callbackURL: "http://127.0.0.1:3000/auth/github/callback",
  },
  async function(accessToken, refreshToken, profile, done) {
    let user = // save or update user with accessToken and profile items
    return done(null, user);
  }
));

همونطور که متوجه شده اید این استراتژی ( استفاده از provider ) نیاز به clientID و clientSecret داره که براساس توضیحی که دادم همون نقش سرویس دهنده ایجاد کننده توکن رو برای ما دارند که باید اونها رو از گیت هاب بگیرید. اینجا میتونید یک اپ بسازید و کلیدهای مورد نظر اون رو دریافت کنید.

گیت هاب کاربر شما رو پس از احراز هویت به لینکی به نام callbackURL میفرسته تا شما از موفقیت آمیز بودن احراز هویت و اون رشته توکن مطلع کنه. پس از این اتفاق شما با استفاده از done() کاربر رو بر میگردونید.

در صورتیکه اطلاعات غلط بود میتونید اینطوری جلوی اون رو بگیرید:

return done(null, false, { message: 'Incorrect credential.' });

خب پس تا اینجا متوجه شده ایم که به دو Route نیاز داریم: اول فرستادن کاربر به گیتهاب برای احراز هویت و دریافت توکن و اطلاعات کاربر در callback پس :

router.get('/auth/github',
  passport.authenticate('github'));
​
router.get('/auth/github/callback', 
  passport.authenticate('github', {session: false}), function(req, res) {
    res.json({email : req.user.email, access_token : req.user.access_token});
  });
​

در صورتیکه احراز هویت به درستی انجام بشه شما accessToken و اطلاعات کاربر احراز هویت شده رو دریافت خواهید کرد.

مقدار توکن میتونه نشون دهنده این باشه که کدام کاربر در حال درخواست دادنه. یعنی از این به بعد کاربر مقدار توکن خودش رو به همراه ریکوئست میفرسته ( مثلا توی header ) و ما توی دیتابیس کاربر متناظر اون توکن رو پیدا میکنیم و برمیگردونیم.

من توی express این بررسی کردن رو اینطوری نوشتم:

async function ensureAuthenticated(req, res, next) {
 var Htoken = req.headers.access_token
 user  = await //find user based on token in db
 if(!user) res.status(401).json({err: "Invalid Request"})
 if(user) {
   req.user = user
   next()
 }
}

و از اون در جاهایی که میخوام دسترسی رو محدود به احراز هویت کنم بعنوان middleware استفاده میکنم:

router.get('/profile', ensureAuthenticated, function(req, res, next) {
  res.json({message: "suceess"})
});