[Firebase Authentication]IDトークンをRevokeさせないユーザー情報更新


はじめに

Firebase Authenticationはドキュメントはかなり充実しているが、
実務で足りない箇所があったため、備忘録的に残しておきます。

実装はGoとAngularで書かれています。

IDトークンのRevokedをチェックする仕組み

フロント側でAuthorizationヘッダーにIDトークンを追加して、
APIのmiddlewareにてAuthoraizationヘッダーを検証します。

auth.ts
@Injectable({
  providedIn: 'root'
})
export class Auth {

  constructor(public afAuth: AngularFireAuth) { }

  // getIdToken IDトークン
  getIdToken(): Observable<string> {
    return this.afAuth.idToken;
  }
}
http.token.interceptor.ts
@Injectable()
export class HttpTokenInterceptor implements HttpInterceptor {

  constructor(private auth: Auth) {}

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    return this.auth.getToken().pipe(
      switchMap((token: string) => {

        if (token) {
          const req = request.clone({
            // tokenがあるならAuthorizationヘッダーに追加
            setHeaders: {
              Authorization: `Bearer ${ token }`
            }
          });
          return next.handle(req);
        }

        // tokenがなければAuthorizationヘッダーなし
        return next.handle(request);
      })
    ).pipe(tap(() => {}
      , async (err: any) => {
        // エラーだったらログアウト
      })
    );
  }
}

middleware.go
package middleware

func checkUser(next echo.HandlerFunc) echo.HandlerFunc {
  return func(c echo.Context) error {
    req := c.Request()

    // Authorizationヘッダーを取得
    token := req.Header.Get("Authorization")

    if token == "" {
      return next(c)
    }

    // IDトークンを取得
    bearerToken := strings.TrimPrefix(token, "Bearer ")

    ctx := context.Background()
    // VerifyIDTokenAndCheckRevokedにてRevokedされているかをチェック
    _, err := a.Authenticator.Client.VerifyIDTokenAndCheckRevoked(ctx, bearerToken)

    if err != nil {
      return err
    }

    // エラーがなければnext
    return next(c)
  }
}

Admin SDKを用いてパスワード、メールアドレスを変更した場合

ユーザーの情報はAdmin SDKの UpdateUser にて更新することができます。
ただし、これを行うとIDトークンがRevokedされるため VerifyIDTokenAndCheckRevoked でエラーとなります

update.go
import "firebase.google.com/go/auth"

// UpdateUser()を用いてユーザー情報を更新すると、IDトークンはRevokedされる
_, err = *auth.Client.UpdateUser(ctx, uid, (&auth.UserToUpdate{}).Password(password))
if err != nil {
  return nil, err
}

reauthenticateWithCredentialを使う

クライアント側でユーザーにpasswordを入れてもらうことで、ユーザーの情報を更新できます。

セキュリティ上重要な操作を行う場合は、直前に認証を行っている必要があります。 → こちら

auth.ts
@Injectable({
  providedIn: 'root'
})
export class Auth {

  constructor(public afAuth: AngularFireAuth) { }

  // updatePassword update current firebase user password
  updatePassword(password: string): Observable<any> {
    const user = this.afAuth.auth.currentUser;
    return from(user.updatePassword(password));
  }
}

constructor(private auth: Auth) {}

updateMethod() {
  const msg = '現在のパスワードを入力';
  const password = prompt(msg);
  if (password === null) {
    return;
  }

  const user = this.auth.afAuth.auth.currentUser;
  const credential = firebase.auth.EmailAuthProvider
    .credential(user.email, password);

  from(user.reauthenticateWithCredential(credential)).pipe(
    mergeMap(() => {
      return this.auth.updatePassword(password);
    })
  ).subscribe(res => {
    // 完了処理
  })
}

まとめ

Firebase Admin SDKを用いてIDトークンがRevokedされるのはわからなくはないが、
それでログアウトされるのもユーザー体験的に嫌なので、クライアント側からパスワードを変更して
IDトークンがRevokedされないようにしました。

もっといい方法があればぜひ教えてください!!