2019/11/21更新
- 2019/10/15以降のInstagram APIの新規利用申込みは停止したようです。 (https://www.instagram.com/developer/ )
- 代替となるInstagram Basic Display API は認証(ログイン)には使えず、ログインに使いたい場合はFacebook loginを使ってくださいとのことでした
@dameee さんありがとうございました!
概要
- Flutter+FirebaseでInstagramログインを実装しました
- FirebaseAuthのドキュメントはこちら(カスタムトークンを作成するパターン)です
- FacebookやTwitterと違い、InstagramについてはFirebase側で連携機構が用意されてない(2019/6/15現在)のでサーバを用意する必要があり、今回はCloud Functionsを使っています。
- Instagramのドキュメントはこちら(Server-side (Explicit) Flow)です
- FirebaseAuthのドキュメントはこちら(カスタムトークンを作成するパターン)です
- InstagramへのログインはWebViewを使います (端末にInstagramのアプリがインストールされていても使われません)
処理の流れ
- アプリ内にWebViewでInstagraのログインページを表示してユーザにログインしてもらう
- ログイン後にCloud Functionsのエンドポイントにcodeリクエストパラメータ付きでリダイレクトされる
- Functions内でcodeを検証(Instagramと通信)してInstagramのユーザID等を取得
- 3で取得した情報を元にFirebaseAuthのカスタムトークンを生成する
- 4で生成したトークンを、Functionsのレスポンスとしてアプリに返す
- 5で受け取ったトークンを使ってFirebaseAuthにログインする
前提
- FlutterにFirebaseを導入済み
- Firebase Cloud Functionsの環境準備済み (今回はTypeScriptを使っています)
- see. https://firebase.google.com/docs/functions/get-started?hl=ja
- ダッシュボードの設定 > サービスアカウント から秘密鍵ファイルをDLしてfunctionsディレクトリ内に設置するのを忘れずに..
準備
-
InstagramのDeveloperページでClient登録し、Client ID, Client Secret を控えておく
- Cloud Functionのendpointは Manage Client > Security の
Valid redirect URIs
に登録する - 検証に使うアカウントは、Manage Client > Sandbox から
Sandbox Users
としてアカウント名を登録する
- Cloud Functionのendpointは Manage Client > Security の
実装
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を使いましたが、とてもお手軽でいいですね!