メモです

メモです

Firestoreに改竄されたくない情報を書き込む

概要

-  ReactみたいなSPAからFirestoreに改竄されたくない情報を書き込む

- クライアント側で書き込むと想定外の部分までユーザが改竄できるため、それを防ぐ

ex. userドキュメントのプロフィールだけを更新したはずが、悪意のあるユーザがUIDフィールドを改竄対象のUIDにしたリクエストをFirestoreに送信することで他人のプロフィールを改竄できる

 

1.  Firebase Functionsで書き込む

Firebase FunctionsではFirebase Authenticationのトークンを検証できるので便利

exports.hoge= functions
.region("asia-northeast1") // region指定
.https.onCall(async (data, context) => {
 
if (!context.auth) {
throw new functions.https.HttpsError(
"failed-precondition",
"The function must be called " + "while authenticated."
);
}

// 話が逸れるがdataをこういう感じでバリデーションすることもできる
if (!(typeof text === "string") || text.length === 0) {
// Throwing an HttpsError so that the client gets the error details.
throw new functions.https.HttpsError(
"invalid-argument",
"The function must be called with " +
'one arguments "text" containing the message text to add.'
);
}

return;
});

 

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のプラグインはあるがぶっ壊れている模様。

 

以下は代表的な関数とか

//create時のチェック
function validateUid() {
return incomingData().Uid == request.auth.uid;
}
 
//update時のチェック
function validateUpdate(giftId){
return request.resource.data.Uid == request.auth.uid;
}
 

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

認証の流れ

  1. Firebase Auth(react-firebaseuiとかで実装する)でTwitte等で認証する。これでFirebaseのIDトークンが生成される。
  2. headerのauthorizationに入ってるFirebaseのIDトークンを取得。FirebaseのIDトークンに入ってるスクリーンネームなどを情報をカスタムIDトークンに入れる
  3. カスタムIDトークンをhttp-onlyのクッキーに入れる(setAuthCookies.js)
  4. サーバ側ではクッキーからIDトークンを取得することで情報を利用。
  5. トークンの検証にはverifyIdTokenを使うことができる(これ用にfirebase-adminを使っている)。APIを保護する(認証済みユーザ以外には使わせない)際などに利用できる(exampleではapi/example.jsに例がある)
  6. logout時はクッキーを削除する

トークンをクッキーに入れる理由

IDトークン(JWT)をlocalStorageに入れることもあるが、localStorageに入れると

  1. XSSに対して脆弱になる

(XSSされる状況でわざわざIDトークンを盗むとかしないだろという反論はある)

  1. 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」に変更する必要がある。

これで時間を失った…。

Labelboxを使ってみた

 ・使いやすかった

・共同作業に強みがあるらしく、複数人でのアノテーションが可能。アノテーションした結果に、評価もつけられる。アノテーション結果をエクスポートすると、各アノテーションに対する点数もついてくるので足切りに使えて便利っぽい。

・2500枚のラベル付与まで無料。対象の物体が画像内に無くてスキップした画像も1枚にカウントされるので注意。

アノテーション 対象の画像をアップするとGCSに保存されて、エクスポートのJSON内にGCSのURLが含まれているので便利

・有料版は月に10万円かかったという事例もあるので高そう。

Pain & Label: Building Our Own Data Labeling Tool | Sixgill

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の純正キーボードを買え。そっちの方が楽。