🍎

Goでfirebaseのカスタムクレームを利用して一定時間有効な特権を与える

2023/06/07に公開

はじめに

表題の通りなのですが、最近うちの開発で「利用しなくなったテナントの全データを削除する」という機能を作りました(それまでは厳重警戒の 2 名体制で直接 DB やストレージ を操作する等を行っていました…!)。

社内向けの管理ユーザー用のサイトにて、テナントのデータを選択すると、GCP 上のリソース(データベース、ストレージ等)がまるっと削除できるという形の機能です。便利になりました。

ただ、この社内向けの管理サイトというのは、現状、認証認可を通ったユーザであれば管理サイト上で何でもできるものになっています。
今回作った機能は強い権限だったため、誰でも削除できる状態というのは危ない!ということで、

  • 削除できるユーザーを制御
  • 削除できる時間を制御

以上の 2 つのことをしてみたいと思いました。

特に 2 つ目の削除できる時間を制御、というものについては、以下の機能を参考にしており、これと似たようなことを実現したかった感じです。
今回はテナントを削除可能な特権ユーザーの権限を付与してから 1 時間という制限を持たせたのですが、この制約を加えることにより、いわゆる権限の剥奪し忘れを防止したいという狙いがありました。

https://cloud.google.com/iam/docs/conditions-overview?hl=ja

実装

社内向けの管理サイトの認証に firebase を利用しているため、Firebase Admin SDK を利用して、有効期限をつけた カスタムクレームを付与するという方法を用いました。
DB に AdminUser のためのテーブルを作成し、そこで管理する…といった方法もありましたが、イケてそうということでこちらになりました。

カスタムクレームとは

カスタムクレームは、以下の通りユーザー情報にさまざまな役割を付与することができます。
ユーザーの状態によって、read/write 権限を付与したり、グループ化して権限を分けたりすることが可能です。
また、カスタムクレームはキーバリュー型で格納できるため、好きなキーを指定することができ、とても柔軟だと思いました。

https://firebase.google.com/docs/auth/admin/custom-claims?hl=ja

実装の流れ

カスタムクレームの付与

ライブラリのauthを利用して、カスタムクレームを付与します。

引数の email はどこかから引っ張ってきてうまく実行する形です。
弊社の場合、一旦は突貫工事的に作ったので、CLI でgrant-privileges -email xxxx@example.comといったコマンドを打った際にこの処理を行うようにしてました。

定義したカスタムクレームprivilegesの値には、今から 1 時間後の時間を入れておき、取り出した際に期限を確認することができるようにします。

func GrantTemporaryPrivileges(email string) error {
    app, err := firebase.NewApp(ctx, nil)
    if err != nil {
        return err
    }
    auth, err := app.Auth(ctx)
    if err != nil {
        return err
    }
    // ユーザー情報を取得する
    u, err := auth.GetUserByEmail(ctx, email)
    if err != nil {
        return err
    }
    // カスタムクレーム"privileges"を1時間だけ付与する
    claims := u.CustomClaims
    if claims == nil {
        claims = map[string]any{}
    }
    claims["privileges"] = time.Now().Add(time.Hour * 1).Unix()
    if err := tenantAuth.SetCustomUserClaims(ctx, u.UID, claims); err != nil {
        return err
    }
}

カスタムクレームの検証

実際のリクエストが来た時の処理が以下です。記述等は書いたコードと違いますが、流れが雰囲気でわかってもらえればありがたいです。
弊社の場合は API リクエストのため、実際はミドルウェアでこの雰囲気のコードを書いて認証を行っています。

// リクエストから取得したtokenを利用
func AuthGrantTemporaryPrivileges(jwt string) (bool error) {
    app, err := firebase.NewApp(ctx, nil)
    if err != nil {
        nil, err
    }
    auth, err := app.Auth(ctx)
    if err != nil {
        nil, err
    }
    // ユーザーをfirebaseで認証する
    authToken, err := a.auth.VerifyIDToken(ctx, token)
    if err != nil {
        return nil, err
    }
    // 必要があればここの付近で追加で諸々情報チェックする
    // ユーザー情報を取得する
    ur, err := auth.GetUserByEmail(ctx, authToken.UID)
    if err != nil {
        return false, err
    }
    // カスタムクレーム"privileges"が付与されているかを検証する
    pe, ok := ur.CustomClaims["privileges"]
    // 付与されていない場合はfalse
    if !ok {
        return false, nil
    }
    exp, ok := pe.(float64)
    if !ok {
        return false, nil
    }
    // 有効期限切れの場合はfalse
    expTime := time.Unix(int64(exp), 0)
    if time.Now().Compare(expTime) > 0 {
        return false, nil
    }
}

おわりに

今はエンジニアがコマンドラインで都度権限を付している状況なので、今後は管理サイトに組み込んだり、slack と連携したりする等を行って、エンジニア以外も適切に権限を管理できるようにしたいなと思っていたりもします!

https://www.wantedly.com/projects/1130967

https://www.wantedly.com/projects/768663

Discussion