メモです

メモです

ローカル端末のAWS CLIのサインインにアクセスキーを使わないほうがいい

AWS CLIのサインイン方法についてググると、アクセスキーとシークレットキーをCSVでDLして設定する方法が出てくる。

しかし、アクセスキーは永続的に利用できる認証情報であり、AWS上でも発行しようとするたびに「多くの場合、期限のない長期のアクセスキー (IAM ユーザーのアクセスキーなど) は必要ありません」と忠告してくる

 

なので、IAM Identity Centerでユーザーを払い出した後に、

aws configure sso

して

SSO start URLにIAM Identity Center→設定→アイデンティティソース→AWS アクセスポータルの URLからコピーしてきたURLを入れるとアクセスキーなしにサインインできる

 

まぁローカル端末でAWS CLIを使わずにCloudShellを使えばよいのですが…

https://dev.classmethod.jp/articles/cli-is-doko/#toc-9

 

 

AWS SESのMAIL FROMドメインは検証済みIDのサブドメインしか指定できない理由

ドキュメントやブログで明言されていないが、「SESから送信されるメールを他から送付されるメールと区別しやすくするため」な気がする
 
前提
カスタムMAILFROMを設定しようとすると検証済みIDのサブドメインしか指定できないと表示される
 
(推測している)理由
SPFDNSルックアップの回数制限があるので、あんまりサブドメインなしのドメインSPFレコードを追加できない
・仮にSESで大量の不達メールを送ってしまった場合、DMARCで指定したメールにレポートメールが届くが、SESだけ個別のサブドメインにしないと他で送ったメールとドメインが一緒になってしまい区別できない
・SESから送るメールだけDMARCの設定を緩くしたいなど柔軟に対応しやすい
・仮にSESで大量の不達メールを送ってしまった場合にもサブドメインから送ればドメイン丸ごと死ぬことが避けやすい(でもこれは未確認なので怪しい)
 
そもそも、機械的かつ大量にメールを送る場合はドメインサブドメインに切り出すというベストプラクティスがあるっぽい
 
ただ、ベストプラクティスといったが、メールを送る場合はサブドメインを許さない(DMARCのStrictモードを使う)ことで、管理外からサブドメインでメールを送られるのを避けるという厳格な運用もあるっぽい
 
メールアドレスに利用できるドメインを、厳密に管理するのが目的です。
• 例えば、strict を指定すれば、勝手にサブドメインでメールアドレスを作れません。
• 現在 relaxed で運用されている組織も、これ以上に勝手にサブドメインの利用が始まると、管理が困難になります
ので、基本的には strict でのアライメント一致を目指すのがオススメです。
※この資料は実務的な面からのフォローが多くて勉強になった
 
雑記
指定したサブドメインにはバウンスメール用のMXレコード1件だけ登録するとかかれているが、最近はSESで受信もできるので、バウンスメール用(feedback-smtp)をと受信用(inbound-smtp)の2件が登録できるっぽい

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のセキュリティルールで、v8では認証状態に応じて値が格納されていたrequest.authが
v9ではrequest.authが常にnullとなっていた

↓こういうルールを実装していた
allow read: if  request.auth != null;

【解決方法】
セキュリティルールを使う場合はgetFirestoreと同時にgetAuthする

const firebaseApp = initializeApp(firebaseConfig)
const db = getFirestore(firebaseApp)

//これを追加する
const auth = getAuth(firebaseApp)

【解決まで】
・ぼんやりと通信を眺めていたところ、v8の通信時にはAuthenticationヘッダーがあるのにv9のときはないことに気づく
・v9でもAuthenticationヘッダーに手動でtokenを付加するとrequest.authに想定通りの挙動をするようになった
→v8のときはfirestoreを呼んだらauthも自動的に呼んでくれたが、v9では必要なコンポーネントのみを呼ぶようになってるから明示的にauthを呼ぶ必要があった

【雑記】
・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