Firebase Authenticationを使ってSSOをしてみた
明けましておめでとうございます!今年も宜しくおねがいします!
さて、独立してからFirebaseをかなり多く触っていますが、今回はFirebase Authenticationを使ってどの様にSSO (Single Sign On / Out) をするかについて書いてみたいと思います。
Firebaseを経験者はご存知だと思いますが、Firebase Authenticationはとても便利な一方、提供機能の中にSSOは含まれていません。なのでWebアプリケーションを複数に分けて作っていて、同じアカウントで認証をさせたい場合は同一のFirebase Projectを使っていたとしてもユーザーは二回認証をする必要があります。最初これを知った時はかなりショックでした。こうなると開発者の都合上アプリケーションを分割するとユーザー体験を大幅に損ねてしまいます。
ここでSSOのニーズがでてきます。弊社でどの様に実現しているかをご参考になれば幸いです。ちなみにこの記事をかなり参考にさせてもらいました。とても良記事です。
目指す体験
Googleのアプリが使いやすいと感じる理由の一つに認証体験の良さがあると思います。Gmail, Drive, Mapなどの複数のアプリを使っていても認証は一度のみすればよいですし、全てのアプリは利用者にパーソナライズされ尽くしていてアプリ間で行ったり来たりできたり、アプリの中に別のアプリが埋め込まれていたりもしているので、あたかも一つのアプリを使っている錯覚さえも起こります。
正直あそこまで高度なことは今回は挑戦しません(てか出来ない)が、みなさんがSSOと聞いて最低限期待するのは以下の様な体験だと思います。
1. アプリAでログインをする。
2. ログアウトせずにアプリBにアクセスした際に、ログインを必要とせずにアプリAでログインしたユーザーとして認証をする (Single Sign On)
3. アプリA or アプリB でログアウトした場合、全てのアプリでログアウトされる (Single Sign Out)
必要なコンポーネント
まずは、Firebase AuthenticationをつかったSSOを実現するために必要なコンポーネントを確認します。
1. SPA - 今回のアプリケーションはAngularやReactなどのSingle Page Applicationを想定しています。
app1.example.com app2.example.com 等でホストされています。
2. Firebase Authentication - これはSDKを通して利用するだけサービスです。認証機能の中核を担ってくれて Firestore や Firebase Functions 等の他のFirebaseのサービスとシームレスに連携します。必要に応じてソーシャルログイン、SMSなどを使った二要素認証、ゲストログインなどの自分で作るには大変な機能も全て提供してくれます。
3. Identity Service - Firebase AuthenticationにSSOの機能を追加するためのサービスです。ブラウザとのセッションを管理するためとCustom Tokenを返します。認証画面を提供できて、HTTPが話せればいいだけなので、どんな言語でどんなサービスをつかって構築してもいいです。Firebase Functionsで作ってよいですが僕は色々SSO以外の役割でも使っているのでCloud Runで作ります。
id.example.com 等でホストされています。
Identity Serviceを使ったセッション管理
認証されていない状態からのフローは以下の様になります。既に認証がされている場合は⑨からです。(クリックで拡大できます。)
重要なポイントは以下だと思います。
• アプリケーションはIdentity Serviceに認証状態を確認する。
• Identity Serviceはブラウザと安全にCookieを使ってSession管理をしている。
• Identity ServiceはSessionが有効なリクエストに対してはCustom Tokenを返す。
• アプリケーションはCustom Tokenを使ってFirebase Authentication認証をする。
横串のIdentity Serviceを使って全てのアプリの認証の際のSession確認を担わせる事ができます。なので、すでにログインされている場合に他アプリケーションからアクセスがある場合はいきなりCustom Tokenが返し即座に認証させる事が出来ますので Single Sign Onが実現されます。
*** 注意 ***
セキュリティのためにCookieの "httponly" と "secure" フラグは必ず true にしておいて下さい。していないと、XSS (クロスサイトスクリプティング)や思わぬ形でセッションがハイジャックされる可能性があります。
サインアウトさせる場合はFirebase AuthenticationとIdentity Serviceの二箇所からサインアウトさせる必要もあります。フローは以下のようなイメージです。Identity Serviceからサインアウトされていれば他のアプリも全て同時にサインアウトされるので Single Sign Outが実現されます。
CORS対応
CORSについては詳しく解説してある記事が山程あると思うので、詳しく見ていただくのが良いと思いますが、
Access-Control-Allow-Origin: app.example.com
Access-Control-Allow-Methods: GET
Access-Control-Allow-Credentials: true
あたりを適切に設定して下さい。右側は例です。
(おまけ) GCP / Firebase においての安全で楽なSession管理について
Session管理をするためにSessionを管理する方法として今回はJSON Web Tokenを利用しました。Session IDを作成してRedisなどに入れて管理してもよかったのですが、極力ステートレスにしたかったので暗号技術にここは頼ります。
JWTを作成する場合は秘密鍵が必要になるので、その鍵を安全に管理する手間が発生するのですが、GCP全般で使えるApplication Default Credentialsを使って楽する方法をご紹介したいと思います。(ちなみにADCがあることによって、個別のサーバーごとにわざわざ認証しなくても安全にGoogle API (BigQueryやCloud Storage等) をIAMで権限付与するだけで利用させることができます。)
ADCが持つ秘密鍵をそのままJWTを署名する際に転用すれば自分が管理する秘密鍵が一つ減るので、楽しながら安全性を高めることができます。今回はCloud Runを使いましたが、App Engine, Cloud Functions, Comute Engineなどの他のコンピュート系サービスでも同じように使えるはずです。
こんな感じJWTを署名できます。
import * as jwt from 'jsonwebtoken'
import { auth } from 'google-auth-library'
const payload = {uid: 'IODM6B85HF'}
const creds = await auth.getCredentials()
const privateKey = creds.private_key
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' })
JWTを検証する際はこんな感じでできます。
鍵のIDとService Accountから公開鍵を取得して、本当に秘密鍵を使って署名されたかどうかの検証をしています。
import * as jwt from 'jsonwebtoken'
import { auth, JWT } from 'google-auth-library'
import axios from 'axios'
const client = await auth.getClient() as JWT
const keyId = client.keyId
const serviceAccount = client.email
const pubCerts = await axios.get(`https://www.googleapis.com/robot/v1/metadata/x509/${serviceAccount}`)
const publicCert = pubCerts.data[keyId]
const decoded = jwt.verify(token, publicCert)
最後に
最後まで読んで頂きありがとうございます! Firebase Authenticationを使ったSSOの構築の仕方を解説しましたが、いかがでしたでしょうか? 弊社都合の実装が多かったので、ソースコードの公開はできませんでしたが、エッセンスが参考になれば幸いです。
Firebase好きな仲間が欲しいので、わからないところ、間違っているところ、改善できるところなどがあればぜひお気軽に連絡ください。それではー!
この記事が気に入ったらサポートをしてみませんか?