11
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Flutter + Firebase(Auth + Cloud Functions) でInstagramログインを実装

Last updated at Posted at 2019-06-15

2019/11/21更新

概要

  • Flutter+FirebaseでInstagramログインを実装しました
  • InstagramへのログインはWebViewを使います (端末にInstagramのアプリがインストールされていても使われません)

処理の流れ

  1. アプリ内にWebViewでInstagraのログインページを表示してユーザにログインしてもらう
  2. ログイン後にCloud Functionsのエンドポイントにcodeリクエストパラメータ付きでリダイレクトされる
  3. Functions内でcodeを検証(Instagramと通信)してInstagramのユーザID等を取得
  4. 3で取得した情報を元にFirebaseAuthのカスタムトークンを生成する
  5. 4で生成したトークンを、Functionsのレスポンスとしてアプリに返す
  6. 5で受け取ったトークンを使ってFirebaseAuthにログインする

前提

準備

  • InstagramのDeveloperページでClient登録し、Client ID, Client Secret を控えておく
    • Cloud Functionのendpointは Manage Client > Security の Valid redirect URIs に登録する
    • 検証に使うアカウントは、Manage Client > Sandbox から Sandbox Usersとしてアカウント名を登録する

実装

1. 必要なライブラリを追加

pubspec.yamlに以下を追加

webview_flutter: ^0.3.9+1

2. ログインページ

1,6の処理です。
JavaScriptChannelでカスタムトークンを受け取る部分がポイントです。

import 'dart:async';
import 'dart:convert';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

// Instagram Developerで発行されたID
const clientId = 'xxxxx';
// Instagram login後にredirectされるURI 今回はCloud Functionsのendpoint
const redirectUri = 'https://xxxxxx.cloudfunctions.net/instagram_auth';
// WebViewで開くInstagramのloginページURL
const loginUrl =
    'https://api.instagram.com/oauth/authorize/?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code';

class LoginPage extends StatelessWidget {
  final Completer<WebViewController> _controller =
      Completer<WebViewController>();
  final FirebaseAuth _auth = FirebaseAuth.instance;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: WebView(
            initialUrl: loginUrl,
            javascriptMode: JavascriptMode.unrestricted,
            onWebViewCreated: (WebViewController webViewController) {
              _controller.complete(webViewController);
            },
            javascriptChannels: [
              // firebaseのtokenを受け取ってログインさせる
              // nameに指定したfunctionがWebView側で呼ばれると、その引数がmessageとして渡ってくる
              JavascriptChannel(
                  name: 'customTokenData',
                  onMessageReceived: (JavascriptMessage message) {
                    final tokenData = jsonDecode(message.message);
                    FirebaseUser fireUser = await _auth.signInWithCustomToken(
                        token: tokenData['token']);
                    // ログイン完了! 別ページに遷移させるなどする
                  })
            ].toSet()));
  }
}

3. FirebaseAuthのカスタムトークンを生成するサーバ側処理

2~5の処理です。
渡ってきたInstagramのコードを検証し、Firebaseのカスタムトークンを発行してアプリに返します。

import * as crypto from 'crypto'
import * as fireAdmin from 'firebase-admin'
import * as functions from 'firebase-functions'
// 以下requireを使ってる箇所は、本当はimportで行いたかったけどエラーが出たりでrequireにした。
// TypeScript力が足りない。。
const serviceAccount = require('/path/to/service-account-file.json')
fireAdmin.initializeApp({
    credential: admin.credential.cert(serviceAccount)
})
const Instagram = require('node-instagram').default
const print = require('pretty-print');

const instagram = new Instagram({
    clientId: 'xxxxxx',
    clientSecret: 'xxxxx',
});

const idSalt = 'aaaaaa' // 適当な乱数
const _instaIdToAppId = (instaUserId: string) => {
    return crypto.createHash('sha1').update(`${idSalt}:${instaUserId}`).digest('base64')
}

const _errorResponse = (response: functions.Response) => {
    const html = `
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <title>example app</title>
    </head>
    <body>
        <p>エラーが発生しました。お手数ですがもう一度やり直してください。</p>
    </body>
</html>
`
    response.send(html)
}

// instagramログイン後にredirectされて来る
//   instagramのcodeを検証し、firebaseのカスタムトークンを作成して返す
export const instagram_auth = functions.region('asia-northeast1').https.onRequest(async (request, response) => {
    const code: String = request.query.code
    // 以下のURIはInstagram Developer側にも登録する
    const redirectUri = 'https://xxxxxx.cloudfunctions.net/instagram_auth'
    let messageToApp: String
    try {
        const res = await instagram.authorizeUser(code, redirectUri)
        // アプリ側のユーザID 
        // InstagramのユーザID+乱数をハッシュ化したものをアプリの(FireAuthの)IDとする
        // (InstagramのユーザIDをそのまま使うのはアプリにユーザ同士の交流機能があったりでユーザIDが露出する場合によろしくないので)
        const appUserId = _instaIdToAppId(res.user.id)
        const token = await fireAdmin.auth().createCustomToken(appUserId)
        messageToApp = JSON.stringify({
            token: token,
            // 以下は無くても良いけどアプリの必要に応じて返す
            name: res.user.username,
            avatarURL: res.user.profile_picture,
        })
    } catch (e) {
        console.log(`[ERROR] instagram auth:`)
        print(e)
        _errorResponse(response)
        return
    }
    // CustomTokenDataというJavascriptChannelを通してアプリ側にデータを渡す
    const html = `
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <script>CustomTokenData.postMessage('${messageToApp}');</script>
        <title>example app</title>
    </head>
    <body>
        <p>ログインしました。しばらくお待ち下さい...</p>
    </body>
</html>
`
    response.send(html);
})

感想

今回はじめてCloud Functionsを使いましたが、とてもお手軽でいいですね!

11
15
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?