こんにちは。今日は認証周りのお話です。
私は今React Nativeでモバイルアプリの開発をしていて、このFacebook認証の機能も使っているのですが、そういえばあまりちゃんと理解してなかったな、と思ったのでこの機会に、もう少しちゃんと理解を深めたいと思い、この記事にまとめることにしました。
同じところで悩んでいる方のお役に立ちましたら幸いです。
#自分でも色々と調べながら書き足していったので、今後もう少し整理したいと思います。
それでは早速参ります。
Contents
一般的なFacebook認証の流れ
Facebook認証のフローは、OAuth2.0のフローに準拠しています。
OAuth2.0のフローについては、以下のQiitaの記事が非常にわかりやすいです。
https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be
つまり、Facebookの認可サーバのエンドポイントに対して、アクセストークンのリクエストを行い、得られたアクセストークンを利用してFacebookのコンテンツサーバへプロフィール情報などを取得する流れとなります。
ReactNaitve+ExpoでFacebook認証を利用するための事前準備
React Nativeに限った話ではありませんが、Facebook認証は、一般的にはFacebookとFirebaseを組み合わせて実現することが多いです。
Firebaseを利用すると、上の認可サーバとのアクセストークンのやりとりをFirebaseに任せることができる、という理解です。
それぞれで必要な設定を確認しておきます。
Facebook側の設定
最初に、Facebook Developerに登録し、そこでアプリ登録をしておく必要があります。
手順の概要はこちら。
https://docs.expo.dev/versions/latest/sdk/facebook/#registering-your-app-with-facebook
登録先はこちら。
https://developers.facebook.com/apps/
Facebookログインを利用するためには、登録したアプリの”Facebookログイン”にて、OAuthリダイレクトURLを登録しておく必要があります。
今回は、ここにfirebaseのOAuthリダイレクトURLにFirebaseのリダイレクトURLを設定しておきます。そのURLを取得するために、ここで一度設定を中断し、Firebase側の設定に移ります。
Firebase側の設定
こちらも詳しい手順は省略しますが、Firebaseに登録し、Authenticationで新しいプロジェクトを追加し、そこでFacebook認証を有効化します。このとき、先ほど作成したFacebookアプリのアプリIDとアプリシークレットを設定します。
逆に、FirebaseのOAuthリダイレクトURIを先程のFacebookアプリのOAuthリダイレクトURIに設定します。
https://firebase.google.com/?hl=ja
この__/auth/handlerって何者だ、という疑問を持たれた方。ここにDiscussionがありました。詳しくはこちらをご参照ください。
結果として、この設定を行うことで、認証は以下のフローで処理されることになります。
- クライアントアプリ
- Facebookアプリの構成情報をもとに、firebaseの__/auth/handlerエンドポイントへリダイレクト
- __/auth/handlerからFacebookの認証エンドポイントに対して認証・トークン取得が行われる
- 認証が完了したらクライアントアプリへ認証情報、トークンが返される
React Native + ExpoでのFacebook認証の実装パターン
expo-facebook
Expoを利用している場合、Facebook認証は、expo-facebookライブラリを用いて行います。
https://docs.expo.dev/versions/latest/sdk/facebook/
以下にサンプルコードの提供があります。
async function logIn() {
try {
await Facebook.initializeAsync({
appId: '<APP_ID>',
});
const {
type,
token,
expirationDate,
permissions,
declinedPermissions,
} = await Facebook.logInWithReadPermissionsAsync({
permissions: ['public_profile'],
});
if (type === 'success') {
// Get the user's name using Facebook's Graph API
const response = await fetch(`https://graph.facebook.com/me?access_token=${token}`);
Alert.alert('Logged in!', `Hi ${(await response.json()).name}!`);
} else {
// type === 'cancel'
}
} catch ({ message }) {
alert(`Facebook Login Error: ${message}`);
}
}
順番に見ていきましょう。
1:initializeAsyncで、SDKの初期化を行っています。
https://docs.expo.dev/versions/latest/sdk/facebook/#initializeasyncoptionsorappid-appname
2:logInWithReadPermissionsAsyncで認証を行なっています。このとき、認証のスコープを引数として渡しています。
https://docs.expo.dev/versions/latest/sdk/facebook/#loginwithreadpermissionsasyncoptions
3:認証が成功したら、取得したアクセストークンを使ってfacebookのGraph APIを叩いて必要な情報を取得しにいきます。
ちなみに、取得したトークンは以下のような形式をしています。このトークンを使うことでFacebookのGraph APIが叩けるようになるということなので、Facebookの認可サーバが発行したものなのだろうけど、JWT形式でもない・・・どういうことだろう(また分かったらアップデートしようと思います)
EAACcab5FJYcBAEgpAZAUS2UEsUaeOdZCGdHDTZBwgFaLLjKKWTUZBoXwciwyv2ZCXcOhdhP7CbZA9AbjG6XBEZAhkUcZCgVGzp9・・・・(中略)・・・・AQDZAvepveBkcrVtdsESXzyiLjLbjIIijkSSnVXj3TSS
ちなみに、Expo Client (Expo Go)アプリ上ではこれで問題なく動きますが、実際にスタンドアプリとして動かす際には、App.jsonにFacebookアプリIDの設定が必要になります。
詳しくはこちらのリンクと記事をご参照ください。
https://docs.expo.dev/versions/latest/sdk/facebook/#configure–appjson
expo-facebook + Firebase SDK Authentication
こちらの方法は、以下のブログで紹介されているのを見かけました。
https://qiita.com/kousaku-maron/items/ce1857208dd9c1ed6a9a
https://tech.mof-mof.co.jp/blog/expo-firebase-facebook-auth/
expo-facebookを利用して、アクセストークン(token)を受け取るところまでは一緒だけど、そのトークンを使ってCredentialを取得して、さらにこれを使ってサインインしてユーザ情報を取得しています。
async function logIn() {
try {
await Facebook.initializeAsync({
appId: '<APP_ID>',
});
const {
type,
token,
expirationDate,
permissions,
declinedPermissions,
} = await Facebook.logInWithReadPermissionsAsync({
permissions: ['public_profile'],
});
if (type === 'success') {
~~~~~~~~~~~~~~
const credential = firebase.auth.FacebookAuthProvider.credential(token)
firebase.auth().signInWithCredential(credential)
~~~~~~~~~~~~~~
.then(()=>{
console.log("Logged In!");
})
.catch((error)=>{
console.log(error);
})
} else {
// type === 'cancel'
}
} catch ({ message }) {
alert(`Facebook Login Error: ${message}`);
}
}
ちなみに、tokenとcredentialは何が違うんだろうと思って中身を見比べてみました。
# token
EAACcab5FJYcBAEgpAZAUS2UEsUaeOdZCGdHDTZBwgFaLLjKKWTUZBoXwciwyv2ZCXcOhdhP7CbZA9AbjG6XBEZAhkUcZCgVGzp9・・・・(中略)・・・・AQDZAvepveBkcrVtdsESXzyiLjLbjIIijkSSnVXj3TSS
# credential
Object {
"oauthAccessToken": "EAACcab5FJYcBAEgpAZAUS2UEsUaeOdZCGdHDTZBwgFaLLjKKWTUZBoXwciwyv2ZCXcOhdhP7CbZA9AbjG6XBEZAhkUcZCgVGzp9・・・・(中略)・・・・AQDZAvepveBkcrVtdsESXzyiLjLbjIIijkSSnVXj3TSS",
"providerId": "facebook.com",
"signInMethod": "facebook.com",
}
結局アクセストークン自体は同じものが返ってきているようですが、credentialの方は、providerIdとsignInMethodのプロパティが追加になっています。firebase.authのsignInWithCredentialメソッドを呼び出すためにはこの形式の引数を渡す必要があったので、取得し直した、ということなのでしょうか。
また、firebase.auth().signInWithCredential(credential)の返り値は、以下の通りuserCredentialの形式となります。ここには、認証プロバイダから返されたユーザ情報等が含まれています。
https://firebase.google.com/docs/reference/js/v8/firebase.auth#usercredential
取得できたuserCredentialオブジェクトの中身のサンプルは以下になります。
Object {
"apiKey": "xxx",
"appName": "[DEFAULT]",
"authDomain": "<firebase project name>.firebaseapp.com",
"createdAt": "xxx",
"displayName": "xxx",
"email": null,
"emailVerified": false,
"isAnonymous": false,
"lastLoginAt": "xxx",
"multiFactor": Object {
"enrolledFactors": Array [],
},
"phoneNumber": null,
"photoURL": "https://graph.facebook.com/<userid>/picture",
"providerData": Array [
Object {
"displayName": "xxxx",
"email": null,
"phoneNumber": null,
"photoURL": "https://graph.facebook.com/<userid>/picture",
"providerId": "facebook.com",
"uid": "xxxx",
},
],
"redirectEventId": null,
"stsTokenManager": Object {
"accessToken": "eyJhbGciOixxxxxx.eyJuYW1lIjoixxxxxxx.roI6CM6Ixxxx",
"apiKey": "xxxxxx",
"expirationTime": 1632023234898,
"refreshToken": "xxxx",
},
"tenantId": null,
"uid": "xxxxx",
}
ユーザのdisplayName, photoURLなど、基本的な情報が取得できていることが分かります。追加で情報の必要が必要であれば、最初の方法の通り、FacebookのGraph APIを個別に叩いて内容を取得する必要がありそうです。
ちなみに、上のサンプルデータを眺めていると、もう一つ新たなAccessToken情報が含まれていることが分かります。これはなんだろう・・?
こちらはJWTトークン形式をしているので、デコードしてみると、以下の情報が確認できました。
ここで”iss”(Issuer = JWTトークンの発行者)を見てみると、”https://securetoken.google.com/<firebaseプロジェクト名>”を指していることが分かります。また、”aud” (Audience = 誰に対して発行されたトークンか)は<firebaseプロジェクト名>をさしていました。
詳細についてはよく分かっていないですが、これはFacebookが発行したトークンではなく、Firebaseが自身の管理のために使うTokenということなのかもしれません。
Expo-Facebook + React-Native-Firebase
Firebase SDK Authenticationを使う代わりに、React-native用のreac-native-firebaseを使う方法もありそうです。
React-Native-FirebaseとFirebase SDK Authenticationの違いは、あまりわかっておらず、Firebase SDK Authenticationを直接使っても実装できるので、MUSTではないのだと思いますが、より効率的にコードを記述できるのかもしれません。(試したことはないので、実装はまたいずれ・・)
探してみると、ちょこちょこと同じ疑問を持っている人を見つけることができました。
以下を見ると、npmのfirebaseは、WebのJavaScript向けのSDKのため、本来はiOSやAndroid上での利用を想定したものではない、というようにも読み取れます。react-native-firebaseは、firebaseのネイティブアプリ向けSDKをWrapしたもの、のようですね。となると、本来はこちらを使った方が好ましいそうですね・・。
そして最終的に欲しかった回答に辿り着きました。
https://teratail.com/questions/285161
やはり、一つ上の議論と同じ結論ですね。今回この記事で実装しようとしているFacebook認証の機能だけでいうと、どちらでも良いのかもしれませんが、広くFirebaseの機能を利用する場合には、react-native-firebaseでしか実現できないこともありそうです。勉強になりました!
以上、React Native + Expo + FirebaseでFacebook認証を実装する方法とその仕組みについて、でした。本日も最後までご覧いただきありがとうございました。内容に関するフィードバックなどありましたら、ぜひコメントいただけますと幸いです。
以下に、もう少しだけおまけが続きますので、よろしければご覧ください!
[番外編] ReactNativeをもっと学ぶなら・・・
React Nativeをもっとちゃんと学びたい!という方は、こちらにおすすめコンテンツをまとめましたので、ご参考にしていただければと思います・・!
土台のJavaScript/TypeScriptに不安がある方は、こちらもまとめましたので、ご参考になりましたら幸いです。
(参考)Githubの情報から見る各ライブラリの人気度合い
expo-facebook
Firebase
*これは、Authenticationだけでなく、全ての機能が含まれる点に注意。ご参考程度に。
React-Native-Firebase
(参考)Firebase SDK Authenticationとは?
https://firebase.google.com/docs/auth
Firebase Authenticationでは、大きく
・FirebaseUI Auth
・Firebase SDK Authentication
の2つの機能が提供されています。
FirebaseUI Authでは、各種プロバイダ認証を行うUI自体が提供されており、個別の処理を記述する必要がないのに対し、Firebase SDK Authenticationでは、各処理は個別に記述が必要ですがより柔軟な実装が可能になります。
Firebase SDK Authenticationを使って、以下のことができます。
- メールとパスワードに基づく認証
- フェデレーションIDプロバイダ(Facebook/Google/Apple…..)との統合
- 電話番号認証
- カスタム認証システムとの統合
- 匿名認証
(参考)アプリコード上の流れ(Web/JavaScript)
Firebaseの公式Docで紹介されている、Facebookログイン+JavaScriptの例でも、実装のフローを確認しておきます。
https://firebase.google.com/docs/auth/web/facebook-login
1:プロバイダ オブジェクトのインスタンスを生成する
var provider = new firebase.auth.FacebookAuthProvider();
2:(省略可)認証プロバイダにリクエストするOAuth2.0の追加のスコープを指定する
provider.addScope('user_birthday');
3:(省略可)認証フローで利用する言語を指定する
firebase.auth().languageCode = 'it';
4:(省略可)追加のOAuthパラメータを追加する
provider.setCustomParameters({
'display': 'popup'
});
5:1でインスタンス化したプロバイダオブジェクトを利用して認証を行う
firebase
.auth()
.signInWithPopup(provider)
.then((result) => {
var credential = result.credential;
var user = result.user;
var accessToken = credential.accessToken; //他のAPIも呼び出す時に使う
// ...
})
.catch((error) => {
var errorCode = error.code;
var errorMessage = error.message;
var email = error.email;
var credential = error.credential;
// ...
});
コメントを残す