[この記事は Nicolas Garnier、Developer Programs Engineer による The Firebase Blog の記事 "Authenticate your Firebase users with Instagram" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。]


Nicolas Garnier
Developer Programs Engineer
Firebase Authentication は、4 つのすぐに使える ID プロバイダをサポートしているため、Google、Facebook、Twitter、GitHub で認証を行うのはとても簡単です。たとえば、ウェブアプリで Firebase ユーザーに Google でログインしてもらうために必要なのは次のコードだけです。

var google = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(google);

ただ、その他の ID プロバイダを使って独自の Firebase アプリにログインしてもらいたい場合もあるかもしれません。たとえば、Instagram API を使ってユーザーが Instagram の写真を共有できるようにするつもりなら、Instagram はすばらしい選択肢になるでしょう。

Firebase にビルトインでサポートされていない ID プロバイダを使うことも可能ですが、それには少しばかりのコードとサーバーが必要になります。ここでは、Instagram によるログインを Firebase ウェブアプリに組み込むために必要な手順について、順に説明します。Instagram はログインに OAuth 2.0 を使っています。そのため、本投稿は LinkedIn のようなその他の OAuth 2.0 ID プロバイダによるログイン機能を組み込む際にも役立つはずです。

設計の概要

Instagram は OAuth 2.0 をサポートしています。これは、アプリの認証や Instagram のユーザー ID を含むユーザーデータにアクセスするための主な方法となります。必要になるのは、ユーザーに OAuth 2.0 認証コードのフローを実行してもらい、アプリへのアクセス権を付与することです。OAuth 2.0 のフローは、次のような流れで実行されます。

まず、ユーザーを Instagram の認証エンドポイントにリダイレクトする必要があります。エンドポイントでは、初めてアプリにアクセス権を付与する際に、ユーザーに同意を求める画面が表示されます。これは、ポップアップ ウィンドウで実現されています。

アプリの認証が完了すると、ユーザーは認証コードとともに元のドメインにリダイレクトされます。Instagram アプリの認証情報を使うと、サーバー側で認証コードアクセス トークンと交換できます。Instagram では、認証コードを交換する過程でユーザー ID も返されます(LinkedIn などの他の OAuth 2.0 プロバイダでは、追加のリクエストが必要になる場合もあります)。

Instagram のユーザー情報を取得すると、サーバーで Firebase のカスタム認証トークンを作成できるようになります。ユーザーは、このトークンと signInWithCustomToken メソッドを使ってウェブアプリで Firebase にログインできます。

また、クライアント上で Firebase のプロフィールをアップデートできるように、表示名や写真の URL などの Instagram から取得したユーザー プロフィール情報も渡しています。 - 注: Instagram は、ユーザーのメールアドレスは提供していません。そのため、メールアドレスなしの Firebase アカウントができることになりますが、このこと自体には特に問題はありません。以上が完了すると、ポップアップが閉じます。これで、ユーザーは Instagram アカウントのプロフィール データつきで Firebase にログインできました。

構築に着手する

では、もう少し詳細に踏み込み、重要な部分の実装方法を見てみましょう。今回は、バックエンドを Node.js で記述します。

Instagram にアプリを登録する

ウェブアプリには、Instagram 認証フローを開始するボタンが必要になります。その前に、OAuth 2.0 に必要なアプリの認証情報を取得できるよう、まず Instagram デベロッパー コンソールにアプリケーションを登録する必要があります。

Instagram アプリの設定で、http://localhost:8080/instagram-callback(テスト用)と https:///instagram-callback(本番用ドメイン)を有効なリダイレクト URI としてホワイトリスト登録しておきます。次に、Instagram クライアント IDクライアント シークレットをメモします。後でこれが必要になります。

アプリを登録し、コールバック URL をホワイトリストに登録すると、クライアント ID とクライアント シークレットが返されます。この流れは、どの OAuth 2.0 プロバイダでも必要となる典型的な手順です。

Instagram の OAuth 2.0 を設定する

サーバーでは、OAuth 2.0 プロトコルの詳細を隠蔽してくれる simple-oauth2 パッケージを使います。これを設定するためには、Instagram クライアント ID とクライアント シークレット、Instagram の OAuth 2.0 トークンと認証エンドポイントなど、いくつかの値を設定する必要があります。Instagram に対して使う必要があるのは、次の値です。

// Instagram OAuth 2 の設定
const credentials = {
 client: {
   id:YOUR_INSTAGRAM_CLIENT_ID, // 要変更
   secret:YOUR_INSTAGRAM_CLIENT_SECRET, // 要変更
 },
 auth: {
   tokenHost: 'https://api.instagram.com',
   tokenPath: '/oauth/access_token'
 }
};
const oauth2 = require('simple-oauth2').create(credentials);

Instagram 認証フローを開始する

ユーザーを Instagram の同意画面にリダイレクトする URL ハンドラをサーバーに追加します。その際に、Instagram 認証フローを完了したときにユーザーがリダイレクトされて戻ってくる場所となるリダイレクト URI を提供する必要があります。今回は、/instagram-callback をコールバック ハンドラのパスに使用します。

app.get('/redirect', (req, res) => {
  // ランダムな状態検証 Cookie を生成
  const state = req.cookies.state || crypto.randomBytes(20).toString('hex');
  // localhost で安全でない Cookie を許可
  const secureCookie = req.get('host').indexOf('localhost:') !== 0;
  res.cookie('state', state.toString(), {maxAge: 3600000, secure: secureCookie, httpOnly: true});
  const redirectUri = oauth2.authorizationCode.authorizeURL({
    redirect_uri: `${req.protocol}://${req.get('host')}/instagram-callback`,
    scope: 'basic',
    state: state
  });
  res.redirect(redirectUri);
});

また、セッション固定攻撃を避けるため、OAuth リクエストの state パラメータでランダムな文字列を渡し、それを HTTP Cookie として保存しています。これによって、戻された state パラメータと Cookie に保存されている値を比較することができ、アプリがフローを開始したことを確認できます。

クライアントでは、ポップアップを表示するボタンとして、次のようなコードを書きます。

function onSignInButtonClick() {
  // ポップアップで Auth フローをオープン
  window.open('/redirect', 'firebaseAuth', 'height=315,width=400');
};

ユーザーがログインボタンをクリックすると、ポップアップがオープンし、ユーザーが Instagram の同意画面にリダイレクトされます。

同意したユーザーには URL の code クエリ パラメータに認証コードが渡され、先ほど渡した state の値と合わせて、/instagram-callback URL ハンドラにリダイレクトされます。

アクセス トークン用の認証コードの交換

ユーザーがコールバック URL にリダイレクトされた際には、以下の処理を行います。
  • state Cookie が URL の state クエリ パラメータと同じであることを確認
  • アクセス トークン用の認証コードを交換し、Instagram からユーザー ID を取得
app.get('/instagram-callback',(req, res) => {
  // state Cookie を受け取ったことを確認
  if (!req.cookies.state) {
    res.status(400).send('State cookie not set or expired.Maybe you took too long to authorize.Please try again.');
  // state Cookie が state パラメータと一致することを確認
  } else if (req.cookies.state !== req.query.state) {
    res.status(400).send('State validation failed');
  }

  // アクセス トークン用に認証コードを交換
  oauth2.authorizationCode.getToken({
    code: req.query.code,
    redirect_uri: `${req.protocol}://${req.get('host')}/instagram-callback`
  }).then(results => {
    // Instagram アクセス トークンとユーザー ID の取得が完了
    const accessToken = results.access_token;
    const instagramUserID = results.user.id;
    const profilePic = results.user.profile_picture;
    const userName = results.user.full_name;

    // ...

  });
});

これでこの実装での OAuth 2.0 固有部分は完成です。以降は、ほぼ Firebase に固有の部分です。

次は、Firebase カスタム認証トークンを作成します。そのカスタム認証トークンを使ってログインを実行し、Firebase のユーザー プロフィールをアップデート(詳細は後述)する HTML ページを提供します。

app.get('/instagram-callback', (req, res) => {

    // ...

  }).then(results => {
    // Instagram アクセス トークンとユーザー ID の取得が完了
    const accessToken = results.access_token;
    const instagramUserID = results.user.id;
    const profilePic = results.user.profile_picture;
    const userName = results.user.full_name;
      
    // Firebase カスタム認証トークンの作成
    const firebaseToken = createFirebaseToken(instagramUserID);

    // ログインを実行し、ユーザー プロフィールをアップデートする HTML ページの提供
    res.send(
        signInFirebaseTemplate(firebaseToken, userName, profilePic, accessToken)));
  });
});

カスタム認証トークンの作成

Firebase カスタム認証トークンを作成するには、Firebase にサービス アカウント認証情報を設定する必要があります。これは、このようなトークンを生成する際に必要となる管理権限を付与するために必要になります。サービス アカウント認証情報ファイルは、service-account.json として保存してください。

const firebase = require('firebase');
const serviceAccount = require('./service-account.json');
firebase.initializeApp({
  serviceAccount: serviceAccount
});

カスタム認証トークンの作成はシンプルです。Instagram のユーザー ID に基づいて uid を選択するだけで構いません。

function createFirebaseToken(instagramID) {
  // ユーザーに割り当てる uid
  const uid = `instagram:${instagramID}`;

  // カスタム トークンの作成
  return firebase.auth().createCustomToken(uid);
}

注: サービス アカウント認証情報は安全に保管する必要があるため、カスタム トークンの作成は必ずサーバー側で行います。

カスタム トークンを作成した後は、それをクライアントに渡して Firebase にログインします。

カスタム トークンによるログイン

この時点で、サーバーはポップアップ ウィンドウ内で実行される HTML ページを提供し、以下の処理を行います。
  • 後ほど Instagram API にアクセスする場合に備え、Instagram アクセス トークンを Realtime Database に保存(注: 対象のユーザーのみが読み取れるようにセキュリティ ルールを使用します)
  • Firebase ユーザー名とプロフィール写真のアップデート
  • ログインの実行とポップアップのクローズ

ここでポイントとなるのは、プロフィールをアップデートする際にデフォルトの Firebase アプリを使うのではなく、一時的な Firebase App インスタンスを使っている点です。これによって、ユーザー プロフィールがアップデートされる前にメインページの Auth リスナーが呼ばれることを防いでいます。

app.get('/instagram-callback', (req, res) => {

    // ...

    // ログインを実行し、ユーザー プロフィールをアップデートする HTML ページの提供
    res.send(
        signInFirebaseTemplate(firebaseToken, userName, profilePic, accessToken)));
  });
});

function signInFirebaseTemplate(token, displayName, photoURL, instagramAccessToken) {
 return `
   <script src="https://www.gstatic.com/firebasejs/3.4.0/firebase.js"></script>
   <script src="promise.min.js"></script><!-- Promise Polyfill for older browsers -->
   <script>
     var token = '${token}';
     var config = {
       apiKey:MY_FIREBASE_API_KEY, // 要変更
       databaseURL:MY_DATABASE_URL // 要変更
     };
     // 一時 Firebase アプリにログインしてプロフィールをアップデート
     var tempApp = firebase.initializeApp(config, '_temp_');
     tempApp.auth().signInWithCustomToken(token).then(function(user) {
    
       // Realtime Database に Instagram API アクセス トークンを保存
       const tasks = [tempApp.database().ref('/instagramAccessToken/' + user.uid)
           .set('${instagramAccessToken}')];
  
       // 必要に応じて displayname と photoURL をアップデート
       if ('${displayName}' !== user.displayName || '${photoURL}' !== user.photoURL) {
         tasks.push(user.updateProfile({displayName: '${displayName}', photoURL: '${photoURL}'}));
       }
  
       // 以上のタスクの完了を待機
       return Promise.all(tasks).then(function() {
         // 一時 Firebase アプリを削除し、デフォルト Firebase アプリにログインし、ポップアップをクローズ
         var defaultApp = firebase.initializeApp(config);
         Promise.all([
             defaultApp.auth().signInWithCustomToken(token),
             tempApp.delete()]).then(function() {
           window.close(); // 完了!ポップアップをクローズ
         });
       });
     });
   </script>`;
}

ポップアップ内でユーザーがデフォルト Firebase アプリにログインすると、認証状態リスナーがメインページを起動し(Firebase では、すべてのタブで認証状態が共有されます)、それですべて完了です。ユーザー プロフィール情報を表示したり、Realtime Database や Firebase Storage を使ったりすることができます。

試してみる

自由に試していてだけるデモアプリを作成しています。https://instagram-auth.appspot.com/

サンプルはオープンソースです。ご自由に Github のリソースをご覧ください。 https://github.com/firebase/custom-auth-samples

Android と iOS への対応

この記事で紹介したコードはウェブアプリ用です。Instagram 認証を Android や iOS アプリに追加するテクニックはいくつかあります。この投稿では紹介できませんが、今後にご期待ください。

これで完了

他の ID プロバイダ用のサンプルを探している場合や、それを組み込む際に問題が発生している場合は、コメントや GitHub リポジトリの問題点でお知らせください。喜んでお手伝いいたします。


Posted by Khanh LeViet - Developer Relations Team