📑

passport-twitter 認証・認可

2022/08/19に公開約8,400字

概要

前回 の続きです!!

サッカー選手の試合成績を集計し、ユーザが口コミ投稿できるアプリを開発中です。

実現したいこと

認証ユーザのみが選手へ口コミ投稿できるように制御したいです。
そのために、今回は Twitter Dev・passport-twitter を用いて認証・認可機能の実装を行います。

Twitter Dev の初期設定

今回のように Twitter 認証を利用する場合、まずは Twitter Dev でプロジェクト/アプリ作成を行います。
https://developer.twitter.com/en/portal/dashboard

下記情報を取得しプロジェクトの環境変数に反映させます。

  • Keys and tokens/Consumer Keys の 2 つ
  • Settings/User authentication settings/Callback URI / Redirect URL
TWITTER_CONSUMER_KEY =
TWITTER_CONSUMER_SECRET =
TWITTER_CALLBACK_URL =

Twitter 認証・認可の実装

  • 認証 ユーザの特定
  • 認可 ユーザの制御

開発環境

Windows 11 Pro
mysql  Ver 8.0.28 for Win64 on x86_64 (MySQL Community Server - GPL)
package.json
"dependencies": {
    "ejs": "^3.1.6",
    "express": "~4.16.0",
    "express-session": "^1.15.6",
    "mysql": "^2.18.1",
    "passport": "^0.4.1",
    "passport-local": "^1.0.0",
    "passport-twitter": "^1.0.4",

手順

  • passport-twitter をインストール

http://www.passportjs.org/packages/passport-twitter/

  • passport-twitter 導入(認証)

middleware の記載箇所の順番等を間違えるとサーバが起動できなくなるので注意しましょう。

app.js
const session = require("express-session");
const passport = require("passport");
const TwitterStrategy = require('passport-twitter').Strategy;
const app = express();
require('dotenv').config();
const env = process.env

// Express settings
app.set("view engine", "ejs");
app.disable("x-powered-by");
...
// ユーザ情報をsessionに格納するための初期化
app.use(session({
  secret: 'secret-key',
  resave: true,
  saveUninitialized: true
}));
// passport自体の初期化
app.use(passport.initialize());
app.use(passport.session());

// passport-twitterの設定
passport.use(new TwitterStrategy({
    consumerKey: env.TWITTER_CONSUMER_KEY,
    consumerSecret: env.TWITTER_CONSUMER_SECRET,
    callbackURL: env.TWITTER_CALLBACK_URL
  },
  // 認証後の処理
  function(token, tokenSecret, profile, done) {
    console.log('profile', profile)
    return done(null, profile);
  }
));

//サーバからクライアントへ応答する際にセッションに情報を保存できる
passport.serializeUser(function(user, done) {
  done(null, user);
});
//クライアントからサーバへ要求する際にセッション情報を復元できる(req.userに保持する)
passport.deserializeUser(function(user, done) {
  done(null, user);
});

// Set middleware
app.use(cookie());
app.use(session({
  store: new MySqlStore({
    ...
  }),
}));

// Dynamic resource rooting.
const indexRouter = require('./app/handlers/index.js');
app.use('/', indexRouter);
app.get('/auth/twitter', passport.authenticate('twitter'));
app.get('/auth/twitter/callback',
  passport.authenticate('twitter', { failureRedirect: '/?auth_failed' }),
  function (req, res) {
    res.redirect('/'), //初期ページ用エンドポイント
    res.redirect('/players'), //選手詳細ページ用エンドポイント
    res.redirect('/auth') //認証用エンドポイント
  });

// Set application log.
app.use(applicationlogger());

// Execute web application.
app.listen(appconfig.PORT, () => {
  logger.application.info(`Application listening at :${appconfig.PORT}`);
});
  • 認証用のエンドポイント処理

認証用エンドポイントのルートにアクセス時、Twitter認証で取得したユーザ情報を取得します。
その後、描画したいEJS(今回はナビゲーションバー)のパスを指定しレンダリングします。

app\handlers\auth.js
const router = require("express").Router();

router.get("/", (req, res, next) => {
  //Twitter認証
  const name = req.user?.displayName;
  const photo = req.user?.photos[0].value;
  res.render("./auth/index.ejs", {name, photo});
});
  • ナビゲーションバーで描画できるようにする

サーバ側から返却されたユーザ情報を下記の仕様でレンダリングできるよう実装していきます。

〇 認証済:Twitter のユーザ名とアイコン画像を表示
✕ 未認証:「Twitterでログイン」ボタンを表示

views_share\navbar.ejs
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarToggler" aria-controls="navbarToggler" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarToggler">
    <ul class="navbar-nav ml-auto">
      <% if (name && photo) { %>
        <li class="nav-item" style="color: white;">
          <h5 class="card-title"><%= name%></h5>
        </li>
        <li class="nav-item">
          <img class="card-img-top" src="<%= photo %>" />
        </li>
      <% } else { %>
        <li class="nav-item">
          <a class="nav-link btn btn-outline-primary my-2 my-sm-0 mr-2" href="/auth/twitter">Twitterでログイン</a>
        </li>
      <% } %>
    </ul>
  </div>
</nav>

  • セッション情報で投稿機能を制御(認可)
app\handlers\form.replys.js
//投稿ボタン押下時の挙動
router.post("/regist/execute", async (req, res, next) => {
  if(!req.session?.passport?.user){
    //未認証ユーザ用のページにリダイレクト
    delete req.session._csrf;
    res.clearCookie("_csrf");
    res.redirect(`../../../auth/tw`);
  }
});
  • 未認証用のエンドポイント処理

未認証用のページにリダイレクトするように制御する

app\handlers\auth.js
router.get("/tw", (req, res, next) => {
  //Twitter認証
  const name = req.user?.displayName;
  const photo = req.user?.photos[0].value;
  res.render("./auth/tw.ejs", {name, photo});
});
  • Twitterログイン誘導ページで描画できるようにする

ナビゲーションバーと同じくTwitterログインをここで誘導します。もちろん前述で説明した通りの認証が通れば認可も可能となるはずです。

views\auth\tw.ejs
<main role="main" class="container">
  <div class="border-bottom mt-5 mb-5">
    <h1>ログインしていただけると. . .</h1>
  </div>
  <div class="row justify-content-center">
      <div class="form-group row">
        <a class="nav-link btn btn-outline-primary my-2 my-sm-0 mr-2" href="/auth/twitter">Twitterでログイン</a>
      </div>
  </div>
</main>
  • CSRF対策

セキュリティ対策のため省略しますが、口コミ投稿時にsession, cookieに格納されます。

おまけ

エラーハンドリング

app.js
// Execute web application.
app.listen(appconfig.PORT, () => {
  logger.application.info(`Application listening at :${appconfig.PORT}`);
});

...

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};
  // render the error page
  res.status(err.status || 500);
});

チートシート

下記リポジトリをクローンすると簡単に Twitter 認証が可能です。
あとは自分用にデータを活用するだけです。
https://github.com/ctyo/express-twitter-sample

このようなデータが返却されるのでフロントに表示したい・認可制御に利用したい等、必要な場合に応じて選定すると良いです。

json: {
    id: ,
    id_str: '',
    name: '今日も一日',
    screen_name: 'ikmz0104mitan',
    location: '南極',
    description: "console.log('どうか表示されてください');",
    url: null,
    entities: { description: [Object] },
    protected: false,
    followers_count: 1407,
    friends_count: 805,
    listed_count: 15,
    created_at: 'Sun Jul 21 01:55:48 +0000 2019',
    favourites_count: 47483,
    utc_offset: null,
    time_zone: null,
    geo_enabled: false,
    verified: false,
    statuses_count: 5519,
    lang: null,
    status: {
      created_at: 'Sun Jul 31 09:27:43 +0000 2022',
      id: ,
      id_str: '',
      text: '',
      truncated: false,
      entities: [Object],
      source: '<a href="https://mobile.twitter.com" rel="nofollow">Twitter Web App</a>',
      in_reply_to_status_id: ,
      in_reply_to_status_id_str: '',
      in_reply_to_user_id: ,
      in_reply_to_user_id_str: '',
      in_reply_to_screen_name: '',
      geo: null,
      coordinates: null,
      place: null,
      contributors: null,
      is_quote_status: false,
      retweet_count: 0,
      favorite_count: 0,
      favorited: false,
      retweeted: false,
      lang: 'ja'
    },
    contributors_enabled: false,
    is_translator: false,
    is_translation_enabled: false,
    profile_background_color: 'F5F8FA',
    profile_background_image_url: null,
    profile_background_image_url_https: null,
    profile_background_tile: false,
    profile_image_url: 'http://pbs.twimg.com/profile_images/1471389834279206914/F5KARvj5_normal.png',
    profile_image_url_https: 'https://pbs.twimg.com/profile_images/1471389834279206914/F5KARvj5_normal.png',
    profile_banner_url: 'https://pbs.twimg.com/profile_banners/1152759045821849600/1652467827',
    profile_link_color: '1DA1F2',
    profile_sidebar_border_color: 'C0DEED',
    profile_sidebar_fill_color: 'DDEEF6',
    profile_text_color: '333333',
    profile_use_background_image: true,
    has_extended_profile: false,
    default_profile: true,
    default_profile_image: false,
    following: false,
    follow_request_sent: false,
    notifications: false,
    translator_type: 'none',
    withheld_in_countries: [],
    suspended: false,
    needs_phone_verification: false
  },
  _accessLevel: 'read'
}

参考

https://qiita.com/c_tyo/items/e2364187265890318361

GitHubで編集を提案

Discussion

ログインするとコメントできます