Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Firebase Authenticationのセッション管理術

Firebase Authenticationのセッション管理術

7/7 PORT Firebase meetup
https://connpass.com/event/285741/
発表させていただいた資料です

SatohJohn

July 07, 2023
Tweet

More Decks by SatohJohn

Other Decks in Programming

Transcript

  1. 株式会社スリーシェイクについて 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領域のプラットフォーマーへ
  2. Cookieシーケンス Login部分 • Firebaseでのログインをした後 idTokenを取得する • idTokenをもとにCookieを サーバで作成してもらう • ログイン状態の2重管理になるた

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

    め以下を注意する ◦ Firebase Authentication の情報保存をoffにするこ と ◦ Cookieの保存後に Firebase Authentication でログアウトすること
  4. 実装(フロント) • ログイン方法は別になんでもよく 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()) };
  5. 実装(サーバ) • フロントから送られてきた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
  6. 実装(サーバ) • 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}
  7. 実装(サーバ) • 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
  8. 実装(フロント) • Firebase Authenticationで ログインをするだけ import { signInWithEmailAndPassword } from

    "firebase/auth"; export const loginWithEmailAndPassword = (auth, email, password, afterLogin) => { return signInWithEmailAndPassword(auth, email, password) .then(async (result) => { return afterLogin(); }) };
  9. 実装(フロント) • 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)); });
  10. 実装(サーバ) • 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!" }); }); });
  11. 実装(フロント) • サーバとの通信とは関係なく ログイン中のユーザの情報を 取得できる • ログイン中のユーザ情報はauthイ ンスタンスから取得する ◦ idTokenから取得できるレ

    ベルではある export const getLoginUser = (auth): Promise<User | null> => { return new Promise((resolve, reject) => { const subscribe = auth.onAuthStateChanged((user) => { subscribe(); resolve(user); }); }); };