Firestoreに改竄されたくない情報を書き込む
概要
- ReactみたいなSPAからFirestoreに改竄されたくない情報を書き込む
- クライアント側で書き込むと想定外の部分までユーザが改竄できるため、それを防ぐ
ex. userドキュメントのプロフィールだけを更新したはずが、悪意のあるユーザがUIDフィールドを改竄対象のUIDにしたリクエストをFirestoreに送信することで他人のプロフィールを改竄できる
1. Firebase Functionsで書き込む
Firebase FunctionsではFirebase Authenticationのトークンを検証できるので便利
2. セキュリティルールでバリデーションする
ただし、様々なバリデーションチェックを実装する必要があり、事故りやすい.
以下の記事を参考にすると良かった
Cloud Firestoreで「いいね」機能を実装するときの勘所 – su- tech blog
注意点
・readとwriteのメソッドは以下の細かなメソッドも含むものなので安易にtrueにしない(writeをtrueにするとユーザはドキュメントを削除できる権限を持つことになる
read: get,list
write: create,update,delete
・updateは要注意。他人のデータを改竄できるようになってないかを確認する
ex. uidフィールドをupdateできてしまう
対策:
- update前後で変更されてはいけないデータをバリデーションする
- hasOnlyでupdateできるフィールドを制限する
・セキュリティルールはVScodeとかで書いた方が楽。formatterのプラグインはあるがぶっ壊れている模様。
以下は代表的な関数とか
3. フィールドそのもの変更をセキュリティルールで禁止する
特定のフィールドへのアクセスを制御する | Firebase Documentation
Firebase v9のFirestoreセキュリティルールでrequest.authが常にnullになる
【問題】
Firebase v9対応をしていたところ、以下のようなFirestoreのセキュリティルールが常にnullと評価されていた
allow read: if request.auth != null;
【解決方法】
firestoreをexporしている箇所で
import { initializeApp } from 'firebase/app' import type { FirebaseOptions } from 'firebase/app' import { getFirestore, setLogLevel } from 'firebase/firestore' import { firebaseApp } from 'utils/firebase' import { getAuth } from 'firebase/auth' const firebaseConfig: FirebaseOptions = { //configをかく } const firebaseApp = initializeApp(firebaseConfig) //これを書く const auth = getAuth(firebaseApp) const db = getFirestore(firebaseApp) export default db
【解決まで】
・v8からv9にアップデートした際に書き方を間違えたかと全く関係ないところを探し回っていた
・通信を眺めていたところ、v8の通信時にはAuthenticationヘッダーがあるのにv9だとないことに気づく
・手動でtokenを付加するとrequest.authfが正しく評価される
↓こんなイージ
const auth = getAuth() const user = auth.currentUser const token = user.getIdToken() setDoc(doc(collection(db, 'users'), ’test’), { name: 'hoge', token: token, })
→v9で必要なコンポーネントのみを呼ぶようになってんじゃんと気づき解決した
【雑記】
・firebaseはindexedDBにtokenを突っ込んでる
・setLogLevel('debug')でfirestoreのデバッグ流せることを知った
Next.jsでFirebase Authenticationをやる(next-firebase-auth)
追記:有名なNextAuht.jsでも同じことができるのでそっちのほうがいいかも、、、。NextAuthは多機能すぎるのでこっちでもいいが、、、
zenn.dev
next-firebase-authの概要
・サーバ側でFirebaseのIDトークンの情報(Twitter認証していたらアイコンとかスクリーンネームが入ってる)が使える
・クライアント側ではconst user = useAuthUser でFirebaseのIDトークンの情報が使える
・firebase v9もbeta版では対応中
・一応、Next.js公式からリンクがある
GitHub - gladly-team/next-firebase-auth: Simple Firebase authentication for all Next.js rendering strategies
認証の流れ
- Firebase Auth(react-firebaseuiとかで実装する)でTwitte等で認証する。これでFirebaseのIDトークンが生成される。
- headerのauthorizationに入ってるFirebaseのIDトークンを取得。FirebaseのIDトークンに入ってるスクリーンネームなどを情報をカスタムIDトークンに入れる
- カスタムIDトークンをhttp-onlyのクッキーに入れる(setAuthCookies.js)
- サーバ側ではクッキーからIDトークンを取得することで情報を利用。
- トークンの検証にはverifyIdTokenを使うことができる(これ用にfirebase-adminを使っている)。APIを保護する(認証済みユーザ以外には使わせない)際などに利用できる(exampleではapi/example.jsに例がある)
- logout時はクッキーを削除する
トークンをクッキーに入れる理由
IDトークン(JWT)をlocalStorageに入れることもあるが、localStorageに入れると
- XSSに対して脆弱になる
(XSSされる状況でわざわざIDトークンを盗むとかしないだろという反論はある)
- Google Analyticsなど第三者が作成したScriptを読み込むことが多いが、それらからも読み取ることが可能になってしまう
(Googleはそんなことしないとは思うが、じゃあどのScriptまでは信用できるのか、第三者が作成するScriptを追加するたびに問題ないかソースコードを全部チェックするのかという問題)
ということらしい。
手順
インストール
git clone https://github.com/gladly-team/next-firebase-auth
cd next-firebase-auth
yarn add next-firebase-auth
yarn add firebase firebase-admin next react react-dom
exampleディレクトリでサンプル用のprojectがあるのでそれをいじっていく
設定
next-firebase-auth/example/.env.local.exampleを開いて
- NEXT_PUBLIC_FIREBASE_PUBLIC_API_KEY
- NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
- NEXT_PUBLIC_FIREBASE_PROJECT_ID
- FIREBASE_PRIVATE_KEY
の4つをfirebaseのコンソール画面の中から探して埋める。
(左上の歯車クリックして、Firebase SDK snippetのところに書いてある)
他は埋めなくても動く。
※NEXT_PUBLIC_FIREBASE_DATABASE_URLはRealtimedatabaseを使う時に使うっぽい?
Firebase Authの設定画面でメールアドレスの認証をオンにする
実行
next-firebase-authのexampleディレクトリに戻り、
npm run dev
を実行する。
localhost:3000を開くと画面が立ち上がっているので、
Example: SSR + data fetching with ID token
とかをクリックして適当なメールアドレスを入力する。
Firebase Authのコンソール画面で見るとユーザができていると思う
なお、もう一度Example: SSR + data fetching with ID tokenを開くとYour favorite color is: が表示されるが、これはapi/example.jsを叩いてるだけなので毎回変わる
なお、api/example.jsではAPI保護のためのIDトークン検証を行っっている。
各ページの内容
static-auth-required-loader.js(Example: static + loader + data fetching with ID token)
クライアント側でデータを取得している。このページに遷移した際の挙動は以下の部分で指定している
whenUnauthedBeforeInit: AuthAction.SHOW_LOADER, whenUnauthedAfterInit: AuthAction.REDIRECT_TO_LOGIN, LoaderComponent: FullPageLoader,
- whenUnauthedBeforeInit : クライアントのJS SDKを初期化する前だった場合、一度ローディング画面を表示する設定。ローティング画面自体はLoaderComponentで指定している。
- whenUnauthedAfterInit : 初期化が終わっているが、ログインされていない場合にはログインページに遷移する設定。
SNS認証
メール認証からTwitter認証に変える場合は
components/FirebaseAuth.jsにある
変更前:
provider: firebase.auth.EmailAuthProvider.PROVIDER_ID,
変更後:
provider: firebase.auth.TwitterAuthProvider.PROVIDER_ID,
に変更する
他のSNSに変えたい場合はこちらのドキュメントを参照する
AuthProvider | JavaScript SDK | Firebase
ちなみにexample.jsはemail情報がないとサインインボタンを表示してしまうため、
各ページのHeaderコンポーネントを
変更前:
<Header email={AuthUser.email} signOut={AuthUser.signOut} />
変更後:
<Header email={AuthUser.id} signOut={AuthUser.signOut} />
に変えるとちゃんとサインアウトボタンが出る。
トラブルシューティング
ページを開いている途中で
Module not found: Can't resolve 'react-firebaseui/StyledFirebaseAuth'
がでたら
npm install react-firebaseui --save --legacy-peer-deps
でなおる
設定
utilsディレクトリのinitAuth.js内で以下を設定できる
- authPageURL : 未ログイン時のリダイレクト先
- appPageURL : ログイン成功時のリダイレクト先
- loginAPIpoint : ログイン用のendpoint
基本的な使い方
クライアント側でログインしているアカウント情報を取得する
import { useAuthUser, withAuthUser } from 'next-firebase-auth' const Demo = () => { const AuthUser = useAuthUser() return ( <div> <p>Your email is {AuthUser.email ? AuthUser.email : "unknown"}.</p> </div> ) } export default withAuthUser({ whenUnauthedAfterInit: AuthAction.REDIRECT_TO_LOGIN, })(Demo)
- AuthUserに今ログインしているユーザの情報が入ってる
- withAuthUserでルートとなるコンポーネントをラップする
- {whenUnauthedAfterInit: AuthAction.REDIRECT_TO_LOGIN,}をつけると未ログイン時はログインページに飛ぶ。ホームページなどログイン状態が関係ないページの場合は消して良い。
Cloud Firestoreとの連携方法
クライアント側
クライアント側でfirestoreからフェッチするやり方はここに書いてある
https://github.com/gladly-team/next-firebase-auth#getfirebaseclient--firebaseapp
サーバ側
サーバサイドでfirestoreからフェッチするやり方。
ログインしたユーザのみに見せたいページの時とかに使う。
withAuthUserを使うことで未ログイン時にはログインページに遷移する。
import { withAuthUser, AuthAction, withAuthUserTokenSSR, getFirebaseAdmin } from 'next-firebase-auth' export const getServerSideProps = withAuthUserTokenSSR({ whenUnauthed: AuthAction.REDIRECT_TO_LOGIN, })(async (context) => { const db = getFirebaseAdmin().firestore() const doc = await db.collection('example').get() // Firestoreから取得したデータを処理する (中略) // クエリ取得 console.log('query', context.query) // トークン検証済みのユーザ情報 console.log('AuthUser', context.AuthUser) return { props: { uid: context.AuthUser.id } } }) const MyLoader = () => <div>Loading...</div> export default withAuthUser({ whenUnauthedBeforeInit: AuthAction.SHOW_LOADER, whenUnauthedAfterInit: AuthAction.REDIRECT_TO_LOGIN, LoaderComponent: MyLoader, })(MainComponent)
ちなみに
・サーバ側でfirestoreからデータを取得
・ログインしていないユーザの場合、閲覧できないようにログイン画面へリダイレクト
のつもりで以下のようなコードを書くと
zenn.dev
にある脆弱性を引き起こすので注意。
※ getServerSideProps = withAuthUserTokenSSRのようにすると何もreturnせずにリダイレクトされる
import { getFirebaseAdmin } from 'next-firebase-auth' // ...other imports const Artist = ({artists}) => { return ( <ul> {artists.map((artist) => <li>{artist.name}</li>)} </ul> ) } export async function getServerSideProps({ params: { id } }) { const db = getFirebaseAdmin().firestore() const doc = await db.collection('artists').get() return { props: { artists: artists.docs.map((a) => { return { ...a.data(), key: a.id } }), } } } export default withAuthUser({ whenUnauthedAfterInit: AuthAction.REDIRECT_TO_LOGIN, })(Artist)
OAuth::Unauthorized 403 Forbiddenが出た
401の場合はAPIキーが間違ってる可能性があるが、403の場合は恐らくTwitterのCallbackURLの設定が間違っている。こっちから投げるrequestのCallbackURLと
Authentication settingsで設定しているCallbackURLが一致してないと403を返す。
なお、RailsのWebConsoleでresponse.bodyを入力するとエラーメッセージがわかる。
エラー画面の下でピョコピョコ動いてるやつ。
ちなみにデフォルトの設定は以下なので、丸パクリしてTwitterのAuthentication settingsで設定すれば良いと思う。
Devise+omniauth+omniauth-twitterの場合
http://localhost:3000/users/auth/twitter/callback
omniauth+omniauth-twitterの場合
http://localhost:3000/auth/twitter/callback
※ドキュメントにはlocalhostにするなって書いてあるけどlocalhostでも認証はできる
※2 omniauthのcallbackのデフォルトの挙動は、遷移元のURLにサフィックスとして/auth/twitter/callbackをつけてcallbackurlとして投げているので、開発環境に127.0.0.1でアクセスしている時はTwitterの設定を「http://127.0.0.1:3000/auth/twitter/callback」に変更する必要がある。
これで時間を失った…。
PythonでYouTubeの動画や音声を保存する
インストール
pip3 install youtube-dl
動画を保存
outtmplでファイル名指定(省略可)
from __future__ import unicode_literals import youtube_dl url = 'https://www.youtube.com/watch?v=ANbGAMsEwSg' ydl_opts = { 'outtmpl':'hoge', } with youtube_dl.YoutubeDL(ydl_opts) as ydl: ydl.download([url])
音声のみを保存
outtmplでファイル名指定(省略可/拡張子必須)
from __future__ import unicode_literals import youtube_dl url = 'https://www.youtube.com/watch?v=ANbGAMsEwSg' ydl_opts = { 'format': 'bestaudio/best', 'postprocessors': [{ 'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192', }], 'outtmpl':'hoge.mp3', } with youtube_dl.YoutubeDL(ydl_opts) as ydl: ydl.download([url])
example
・リストで一括ダウンロード
・音声だけを取り出す
・ファイル名は動画のタイトル
from __future__ import unicode_literals import youtube_dl url = ['https://youtu.be/TplaQhVNUKk','https://www.youtube.com/watch?v=CBKQoqTI2iE'] ydl_opts = { 'format': 'bestaudio/best', 'postprocessors': [{ 'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192', }], 'outtmpl':'%(title)s.mp3', } with youtube_dl.YoutubeDL(ydl_opts) as ydl: ydl.download(url)
exampl2
・動画のタイトルや再生回数などのメタデータだけ取得する
・extract_infoはurlのリストを受け付けないので注意
import youtube_dl url = 'https://www.youtube.com/watch?v=2tA1rVKv4EE' ydl_opts = { 'writeautomaticsub': 'False', } with youtube_dl.YoutubeDL(ydl_opts) as ydl: res = ydl.extract_info(url, download=False) print(res['title']) # タイトル print(res['view_count']) # 視聴回数 print(res['automatic_captions']['ja']) # 自動生成の日本語字幕
MacでJISキーボードを使う
・Karabiner-Elementsをインストール
・Karabiner-Elementsを起動してsimple-modificationsでFrom Keyに「grave_accent_and_tilde」をTo keyに「F13」を設定する
・システム環境設定(歯車のアイコン)を起動し、キーボードを開く
・キーボードの種類を変更を押下し、指示に従う。
・次に「修飾キー」というボタンを押下。ControlキーをCommandに設定する。
以上で半角/全角キーでかな英字の切替が可能になると同時に、Ctrlキーが機能する様になる
が、普通にMacの純正キーボードを買え。そっちの方が楽。