Slide 1

Slide 1 text

Firebase Authenticationの セッション管理術 2023/07/07 PORT Firebase meetup 株式会社スリーシェイク 佐藤慧太

Slide 2

Slide 2 text

自己紹介 ● 株式会社スリーシェイク Sreake事業部 ● 佐藤慧太@SatohJohn ● SREとしてお客様の労苦を減らす仕事 ● Google Cloudの製品が好き ● FirebaseでのWebアプリケーション経験4年ぐらい

Slide 3

Slide 3 text

株式会社スリーシェイクについて Copyright © 3-shake, Inc. All Rights Reserved.    xOps Plattform DesignOps IaaS DevOps / SRE RevOps (Revenue Ops) HR(Engineer Hiring) HROps Data Engineering DataOps Security DevSecOps SecOps 事業者が抱える セキュリティリスクを無くす 本格的な脆弱性診断を 無料で手軽に セキュリファイ Security 良いエンジニアに良い案件を フリーランスエンジニアに 「今よりいい条件」を リランス HR(Engineer Hiring) あらゆるサービスを連携する ハブになる クラウド型ETL/データパイプ ラインサービスの決定版 レコナー Data Engineering 日本のSREをリード SRE総合支援からセキュリティ 対策を全方位支援 スリーク SRE スリーシェイク = xOps領域のプラットフォーマーへ

Slide 4

Slide 4 text

Session管理って めんどくさいですよね?

Slide 5

Slide 5 text

本日伝えたいこと ● Webアプリケーションでの Firebase Authenticationのセッション管理方法について ○ Cookie ○ idTokenとService Worker

Slide 6

Slide 6 text

本日伝えたいこと ● Webアプリケーションでの Firebase Authenticationのセッション管理方法について ○ Cookie ○ idTokenとService Worker 実装できる(気がする)まで持っていく

Slide 7

Slide 7 text

話さないこと ● Service workerの詳しい説明 ○ fetchしかでてきません ● Firebase Authenticationの詳しい説明 ○ Login後のどう管理するかだけです

Slide 8

Slide 8 text

超概略的まとめ ● idTokenはaccess tokenのようなもので、有効期限がとても短い ● idTokenをcookieに変換して利用する ○ サーバ側で検証、処理をするイメージ ● service workerを使って、idTokenのまま利用する ○ フロント側で検証、処理をするイメージ

Slide 9

Slide 9 text

Cookieの利用

Slide 10

Slide 10 text

Cookieでの管理のメリット ● ブラウザが保存、取り出しをしてくれるため フロントで処理を記載する必要がない ● 初回アクセス時にサーバ側で処理できる ● 古いブラウザでも対応している

Slide 11

Slide 11 text

Cookieでの管理のデメリット ● Cookieの作成の口を用意しないといけない ● 色々なアプリで展開している場合、Cookie用の検証用の口を サーバサイドで用意しないと行けない ● 他Firebase製品をフロントで使う場合や、Password変更など 組み合わせる際に、セッション管理が別になり、複雑になる

Slide 12

Slide 12 text

Cookieシーケンス Login部分 ● Firebaseでのログインをした後 idTokenを取得する ● idTokenをもとにCookieを サーバで作成してもらう ● ログイン状態の2重管理になるた め以下を注意する ○ Firebase Authentication の情報保存をoffにするこ と ○ Cookieの保存後に Firebase Authentication でログアウトすること

Slide 13

Slide 13 text

Cookieシーケンス Login部分 ● Firebaseでのログインをした後 idTokenを取得する ● idTokenをもとにCookieを サーバで作成してもらう ● ログイン状態の2重管理になるた め以下を注意する ○ Firebase Authentication の情報保存をoffにするこ と ○ Cookieの保存後に Firebase Authentication でログアウトすること

Slide 14

Slide 14 text

実装(フロント) ● ログイン方法は別になんでもよく idTokenが取得できれば良い ● Cookie取得のリクエストが成功す ればCookieが自動的に ブラウザに保存される import { signInWithEmailAndPassword, } from "firebase/auth"; export const loginWithEmailAndPassword = (auth, email, password) => { return signInWithEmailAndPassword(auth, email, password) .then(async (result) => { const idToken = await result.user.getIdToken(); return login(idToken); }) }; export const login = (idToken) => { return fetch("/auth/authenticate", { method: "POST", data: { id_token: idToken, }, }).then((data) => data.json()) };

Slide 15

Slide 15 text

実装(サーバ) ● フロントから送られてきたidToken を使ってCookieを 組み立てる ● CookieにHTTP OnlyとSecure属 性をつけることを忘れない def authenticate(request): id_token = request.POST.get("id_token") if id_token is None or len(id_token) == 0: return HttpResponse("id token not found", status=400) session_cookie = create_session_cookie(id_token) if session_cookie is None: return HttpResponse("Failed to create a session cookie", status=401) response = HttpResponse() response.set_cookie( session_cookie["name"], value=session_cookie["value"], expires=session_cookie["expires"], secure=True, httponly=True, samesite="Lax", ) return response

Slide 16

Slide 16 text

実装(サーバ) ● idTokenをデコードすると認証した 時間が取れる ● 認証した時間から公式では 5分以内であることで不正ではな いことを担保している ● Cookieで使える値は最大14日間 の有効期限なのでCookieもその 時点で消えるようにしておく from firebase_admin import auth def create_session_cookie(id_token): decoded_claims = auth.verify_id_token(id_token) if time.time() - decoded_claims["auth_time"] < 5 * 60: expires_in = datetime.timedelta(days=14) expires = datetime.datetime.now() + expires_in session_cookie = auth.create_session_cookie(id_token, expires_in=expires_in) return build_session_cookie(expires, session_cookie) return None def build_session_cookie(expires, session_cookie): return {"expires": expires, "value": session_cookie, "name": LOGIN_COOKIE_NAME}

Slide 17

Slide 17 text

cookieシーケンス リクエスト部分 ● ブラウザがリクエスト時にCookie をつける ● 検証結果に応じて処理を 実施する

Slide 18

Slide 18 text

実装(サーバ) ● Cookieからclaim取得して それからUserを取得する ● userが取得できない場合は 403エラーとしても良いし 未ログインでも使えるように してもよい ● Pythonの例ではあるが FrameworkのMiddleware上で 実装するのが良い class RequiredAuthorizedMiddleware: get_response = None REQUIRED = "required" KEY = "auth" REQUIRED_DICT = {KEY: REQUIRED} def __init__(self, get_response): self.get_response = get_response def process_view(self, request, view_func, view_args, view_kwargs): claim = auth.verify_session_cookie(request.COOKIES.get(LOGIN_COOKIE_NAME)) if self.authorized(claim, request): request.login_user = convert_login_user(claime) return None return HttpResponse("fobidden", status=403) def authorized(self, claim, request): if claim is None: return False return True

Slide 19

Slide 19 text

idTokenとservice worker

Slide 20

Slide 20 text

idTokenとservice workerのメリット ● 作成の変換コストがcookieより少ない ● client、Firebase間でidTokenの更新をするため、サーバ側での期限切れがパ ターンが少ない ● idToken作成に使うrefresh tokenがcookieよりも生存する期間が長いため 再度Loginページ表示が少なくて済む ● iOS、Androidと同じような処理で、サーバ側で認証ができる

Slide 21

Slide 21 text

idTokenとservice workerのデメリット ● idTokenの期限が1時間なので、作り直しはcookieより多い ● 古いブラウザなどでService Workerに対応していない可能性がある ● 初回アクセス時にサーバ側で処理しないため、要件とは合わない可能性がある

Slide 22

Slide 22 text

service worker シーケンス ● Cookieのときと、ログインまでは 変わらない ● HTMLを受け取りservice worker を、インストールする部分が入って いる

Slide 23

Slide 23 text

実装(フロント) ● Firebase Authenticationで ログインをするだけ import { signInWithEmailAndPassword } from "firebase/auth"; export const loginWithEmailAndPassword = (auth, email, password, afterLogin) => { return signInWithEmailAndPassword(auth, email, password) .then(async (result) => { return afterLogin(); }) };

Slide 24

Slide 24 text

service worker シーケンス ● idTokenが取得できない場合は Loginページに遷移させる ○ サーバで処理する場合も 検証は必要なので 2箇所見る必要がある

Slide 25

Slide 25 text

実装(フロント) ● getIdTokenの際にtokenが取得で きるかをみて リダイレクトさせる ● httpRequestの際にリクエストをプ ロキシすることができるので同じ ホストの場合 Headerに追加する ● 公式で公開されているコードがあ るのでそちらを見ながら 触ってみると良いかと self.addEventListener("fetch", (event: Event) => { const fetchEvent = event as FetchEvent; const requestProcessor = (idToken: string | null) => { let req = fetchEvent.request; let processRequestPromise = Promise.resolve(); if (self.location.origin == getOriginFromUrl(fetchEvent.request.url)) { const headers = new Headers(); for (let entry of req.headers.entries()) { headers.append(entry[0], entry[1]); } headers.append("Authorization", "Bearer " + idToken); processRequestPromise = getBodyContent(req).then((body) => { req = new Request(req.url, { method: req.method, headers: headers, mode: "same-origin", credentials: req.credentials, cache: req.cache, redirect: req.redirect, referrer: req.referrer, body: body as string, }); }); } return processRequestPromise.then(() => fetch(req)); }; fetchEvent.respondWith(getIdToken().then(requestProcessor)); });

Slide 26

Slide 26 text

実装(サーバ) ● headerからidTokenを取得し フロントで行えないユーザ別の処 理を行う事ができる ● この形はアプリでも同じ形に なるはず function getIdToken(req) { const authorizationHeader = req.headers.authorization || ''; const components = authorizationHeader.split(' '); return components.length > 1 ? components[1] : ''; } app.get("/something", (req, res) => { const idToken = getIdToken(req); admin .auth() .verifyIdToken(idToken) .then((decodedClaims) => { return res.json(something(decodedClaims)) }) .catch((error) => { console.log(error); res.status(403); res.json({ error: "You must be logged in to continue!" }); }); });

Slide 27

Slide 27 text

実装(フロント) ● サーバとの通信とは関係なく ログイン中のユーザの情報を 取得できる ● ログイン中のユーザ情報はauthイ ンスタンスから取得する ○ idTokenから取得できるレ ベルではある export const getLoginUser = (auth): Promise => { return new Promise((resolve, reject) => { const subscribe = auth.onAuthStateChanged((user) => { subscribe(); resolve(user); }); }); };

Slide 28

Slide 28 text

まとめ ● どっちが良いということはなく、サービスの特性に合わせて利用する ○ idTokenをcookieに変換して利用する ■ 処理をサーバで実施するようなパターン ■ MPAのアプリケーションと相性が良い ○ service workerを使って、idTokenのまま利用する ■ 処理をフロントで実施するようなパターン ■ SPAのアプリケーションと相性が良い

Slide 29

Slide 29 text

一緒にSREとして働きませんか? ● 社会から労苦をなくしましょう ○ 気になりましたら会社ホームページやどんなメンバがいるのか を見ていただき 申し込みしていただければ! ○ または、私の方にtwitterとかでメンションなどでご相談していただければ!

Slide 30

Slide 30 text

おわり