iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 28
0
Modern Web

Node JS-Back end見聞錄系列 第 28

Node.js-Backend見聞錄(27):進階實作-關於第三方登入(二)

Node.js-Backend見聞錄(27):進階實作-關於第三方登入(二)

前言

我們在進階實作-關於第三方登入(一)的文章中,已經講述完關於oauth2及Facebook第三方登入的運行方式。接續,我們試著用Node.js來實作這部分的功能。

第三方登入功能需求

  • 試著在頁面中呈現用戶姓名。

開始實作

在實作前,我們先去Facebook的開發人員頁面去申請個APP 來使用,因為若沒該申請APP我們也沒辦法使用它的服務。

申請Facebook APP

首先,先點選該頁面右上角的我的應用程式,並選擇當中的新增應用程式選項。

之後,建立一個你想取的APP名稱,並按建立應用程式編號按鈕。

接續選取Facebook登入的功能。

再來,進入到主控版,記取通用程式編號通用程式密鑰,這兩個東西待我們在撰寫第三方登入時會使用到。

撰寫第三方登入程式

在Node.js中,有套件可以協助我們來開發Facebook的第三方登入。

套件

  • passport:快速提供第三方登入服務的套件。
  • passport-facebook:在passport中,分有多種Strategy。而passport-facebook就是passport眾多Strategy中的一種,專門用來提供連接facebook的套件。
  • express-session:由於passport-facebook套件的做法是會將由Facebook那邊取回來的token及用戶資料存在session中,所以我們需要再額外使用這個套件來記取token。
$ npm install passport
$ npm install passport-facebook
$ npm install express-session

資料結構

這部分我們依舊透過Express應用程式產生器來開啟一個專案,並將結構分成:

.
├── app.js
├── bin
│   └── www
├── config
│   └── passport.js
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   ├── login.js
│   └── users.js
├── views
    ├── error.ejs
    ├── index.ejs
    └── success.ejs
├── .env
└── .gitignore

剖析運行流程

這部分,筆者會根據進階實作-關於第三方登入(一)文章中提到的Facebook Oauth2的運行流程,來跟passport-facebook所提供的範例程式碼做搭配說明。

     +--------+                               +---------------+
     |        |--(A)- Authorization Request ->|   Resource    |
     |        |                               |     Owner     |
     |        |<-(B)-- Authorization Grant ---|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(C)-- Authorization Grant -->| Authorization |
     | Client |                               |     Server    |
     |        |<-(D)----- Access Token -------|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(E)----- Access Token ------>|    Resource   |
     |        |                               |     Server    |
     |        |<-(F)--- Protected Resource ---|               |
     +--------+                               +---------------+

                     Figure: Facebook Protocol Flow
(A)- Authorization Request

passport-facebook套件中,這部分的處理會是放在:

app.get('/auth/facebook',
  passport.authenticate('facebook'));

也就是前端的開發夥伴可以透過/auth/facebook的API,來連接到Facebook那邊來顯示像下述圖的狀況。讓Facebook的使用者來做個同意授權的動作。

figure from: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/

待使用者同意授權後,就會取得(B)-- Authorization Grant步驟的認證資料。

(C)-- Authorization Grant

待server端這邊拿到認證資料後,會再透過下列的function來做處理:

passport.use(new FacebookStrategy({
    clientID: FACEBOOK_APP_ID,
    clientSecret: FACEBOOK_APP_SECRET,
    callbackURL: "http://localhost:3000/auth/facebook/callback"
  },
  function(accessToken, refreshToken, profile, cb) {
    User.findOrCreate({ facebookId: profile.id }, function (err, user) {
      return cb(err, user);
    });
  }
));

可以由上述的程式碼看到passport使用了Facebook的Strategy,且在申請Facebook APP階段,筆者有提到要讀者記取的通用程式編號通用程式密鑰會在這部分用到。

  • clientID中放入通用程式編號。
  • clientSecret中放入通用程式密鑰。

callbackURL指的是在取得用戶同意授權後,會在導向到該URL中。

註記:這部分的步驟等同於上篇文章中所提到的由Facebook所提供的API來呼叫的C: Client端要向authorization server來索取access token。部分是一樣的。

但方便的是在這步驟它一起幫我們解決了(D), (E)(F)--- Protected Resource的動作。該步驟的處理不僅僅只有取得accessToken,也會將需要透過token才能取得的受保護的敏感性資料(Facebook用戶資料)一併匯入到該function的cb(callback)上。但該套件會將cb的資料放置session中,所以也額外提供了:

passport.serializeUser(function(user, cb) {
  cb(null, user);
});

passport.deserializeUser(function(obj, cb) {
  cb(null, obj);
});

這部分的意思是它將用戶資料給serialize(序列化)並再透過deserialize(反序列化)的方式來將用戶資料放置session中。

最後,在上面有提到的callbackURL所做的重導向(redirect)動作,則是在這function中做處理:

app.get('/auth/facebook/callback',
  passport.authenticate('facebook', { failureRedirect: '/login' }),
  function(req, res) {
    // Successful authentication, redirect home.
    res.redirect('/');
  });

也就是若用戶同意授權則會轉到/的這個URL中,若不同意則會轉到/login的URL中。

程式部分

API URL

我們這部分會用後端render的方式來進行頁面開發。因為這部分若要測試只能實際寫個頁面來測試,Postman並沒有幫法協助這部分的測試。等同於說頁面的使用流程會是:

    +------------+                         +-------------+
    | index.html | ------ 成功登入 ------>  | success.html |
    +------------+                         +-------------+

匯入到routes資料夾的login.js檔案:

var express = require('express');
var router = express.Router();

const passport = require('../config/passport');

router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});

router.get('/success', function(req, res, next) {
    // console.log(req.user);
    res.render('success', {data: req.user});
})

router.get('/auth/facebook', passport.authenticate('facebook'));

router.get('/auth/facebook/callback',
  passport.authenticate('facebook', { successRedirect: '/success',
                                      failureRedirect: '/' }));

module.exports = router;

Passport config

config資料夾的passport.js檔案中寫入:

var passport = require('passport')
    , FacebookStrategy = require('passport-facebook').Strategy;

passport.use(new FacebookStrategy({
    clientID: FACEBOOK_APP_ID,
    clientSecret: FACEBOOK_APP_SECRET,
    callbackURL: "http://localhost:3000/auth/facebook/callback"
},
    function (accessToken, refreshToken, profile, done) {
        return done(null, profile);
    }
));

passport.serializeUser(function (user, cb) {
    cb(null, user);
});

passport.deserializeUser(function (obj, cb) {
    cb(null, obj);
});

module.exports = passport;

註記:clientID及clientSecret請寫入讀者自行申請到的編號及密鑰。

設置session

session的部份我們需要到app.js中做設定,才會讓我們的專案有session可運行。

var passport = require('passport');

// =========
// ...
app.use(express.static(path.join(__dirname, 'public')));
// =========
// 請自行在該行底下新增下列三行code

// passport-middleware
app.use(require('express-session')({ secret: 'keyboard cat', resave: true, saveUninitialized: true }));
app.use(passport.initialize());
app.use(passport.session());

Web view

為了測試,我們就寫個簡陋的前端Code...(真的非常簡陋)。到views資料夾的index.ejs檔案中寫入:

<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>
    <h1><%= title %></h1>
    <p>Welcome to <%= title %></p>
    <a href="/auth/facebook">Log In with Facebook</a>
  </body>
</html>

接續在success.ejs檔案中寫入:

<!DOCTYPE html>
<html>
  <head>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>
    <h1>hi, <%=data.displayName%></h1>
  </body>
</html>

測試

首先,我們先用npm start的方式開啟專案,之後開啟瀏覽器並輸入localhost:3000會看到:

點選剛剛所寫好的超連結部分Log In with Facebook後會看到:

這代表我們在Facebook APP那邊的設定還沒有完全。先到Facebook 登入點選設定按鈕,並在右邊的有效的OAuth重新導向URL中輸入http://localhost:3000/auth/facebook/callback

之後,再回到一開始的localhost:3000頁面,並點選超連結部分後就會看到:

按下確認鈕後。

就成功看到我們用戶自己的displayname了!

額外發現

順帶一提,筆者發現Facebook的政策有改變,如果想要將APP對外發佈的話目前需要額外再輸入有該APP的「隱私政策網址」。不像之前想發佈就可以直接發佈,筆者在猜可能是為了用戶的安全考量才會有這政策。

但有趣的是它驗證是否是有效的隱私政策網址的方式是看domain連過去的實體IP有沒有效。變成說就算筆者輸入https://google.com也會通過(笑),不過假設真的要發佈這個APP,筆者相信應該大家都還是會用自己網站的網址才是。

小結

各家社群網站的第三方登入服務其規範或多或少會有些許不同,像Facebook本身其實沒有提供refresh token,但像Google就有提供。所以,讀者若想要嘗試實作其它社群網站的第三方登入功能,不仿先從該社群網站所提供的API文件看起。

註記:關於refresh token可參考RFC-6749-1.5. Refresh Token

繼說明原理並接續基本實作後,不知道讀者對於第三方登入有沒有更進一步的理解?接下來,我們將前進金流的進階實作部分。


上一篇
Node.js-Backend見聞錄(26):進階實作-關於第三方登入(一)
下一篇
Node.js-Backend見聞錄(28):進階實作-關於金流
系列文
Node JS-Back end見聞錄31

尚未有邦友留言

立即登入留言