🗝️

Firebase JWT による Golang/Flutter 認証アプリ

2024/07/28に公開
3

Firebase Authentication はGoogleが提供する認証システムで、メール認証、Google認証など様々な形式に対応しています。Firebase AuthenticationはJWT (Json Web Token) を使用して認証情報をクライアントに渡し、サーバーサイドでそのJWTを検証しAPIレスポンスを行うことができます。
今回は GolangとFlutterでその流れを アクセストークンを受け取って 有効期限が切れたら、リフレッシュトークンで新しいアクセストークンに更新しサーバー側にAPIリクエストを送るまでをやって行こうと思います。

こんな人におすすめ

・DartかGolangでどちらかの開発経験がある
・JWTの実装手順を知りたい
・backend から frontendまで一貫した認証情報の流れを見たい

⭐️ 認証周りのコーディングをフルスタックで組めるようになります

jwtの構造を抑えよう

JWTは、ドット ( . ) で区切られた次の3つの部分で構成されています。

  • ヘッダー
  • ペイロード
  • 署名

それぞれみていきましょう。

# ヘッダー
{
  "alg": "HS256",  # ハッシュアルゴリズム
  "typ": "JWT"     # トークンのタイプ
}
# ペイロード
{
  "sub": "1234567890",       # ユーザ識別子。user_idといったユーザを識別するためのID
  "exp": 1234567890,         # JWTの有効期限
  "iss": "hoge_service",     # JWTの発行者
  "iat": 1234567890,         # JWTの発行日時
  "email": "test@kmail.com", # プライベートクレーム original
}

署名
ヘッダーとペイロードを連結し、秘密鍵(secret)を使ってHMAC等のハッシュアルゴリズムで署名したもの。もしHeaderかPayloadのデータが改竄されていれば、JWT検証時に署名を用いて復号ができなくなるため、改竄を検知することができる。

なので、おおまかには、データが改竄されていないことを確認し
認証情報を適切に管理する。といった理解でとりあえずokです。

firebase jwtでは emailやgoogle認証によりサインイン(アップ)時にトークンが付与されます。しかしその有効期間は短いです。それがきれたときにreflesh tokenが必要になり、
認証情報を適切に管理していこうといった流れです。

出てくる用語の整理

認証サーバ

firebase jwtを使用するときはfirebaseのインフラに存在。
認証が成功するとアクセストークンとリフレッシュトークンを発行。

リソースサーバ

保護されたリソースを提供する。backend側のサーバー。

アクセストークン

短期間有効なトークン。

リフレッシュトークン

長期間有効なトークンでアクセストークンの有効期限が切れたときに新しいアクセストークンを認証サーバに問い合わせるときに必要。

カスタム トークン

リフレッシュトークンをfirebaseの 認証サーバに要求するときに必要。
※初めての時?となりやすいので公式のリンクをつけておきました。

jwtの流れを抑えよう

まず、こちらが今回紹介する firebase jwtの全体のながれとなります。

ここで重要なポイントは、リソースサーバでアクセストークンの検証が行えるという点です。
一般的なアクセストークン認証の場合、リソースサーバは認証サーバにアクセストークンが正しいか問い合わせます。
ここがjwtをつかう大きなメリットといえるでしょう。
以下では、
Frontend にFlutter
Backend に Golangを使用した例を実際のコードを交えながら見ていきます。
ほかの言語を使用されている方でも十分に参考になると思います。

リポジトリについて

プロジェクトのリポジトリは公開しております。
こちらのコードを用いて説明いたします。
https://github.com/rensawamo/authentication_app

以下のように、ボタンをタップするだけで loggerを入れ込み、処理の流れが把握できるようにしております。

環境変数とFirebase環境構築

  • frontend と backend の .env.exampleを .envに変えて
    自分の情報をいれます。

API_KEY は こちらが参考になると思います。

FIREBASE_ACCOUNT_KEY_LOCATION はこちらを参照してください。

  • Flutter側で firebaseの環境構築をする

手順

1. サーバを立ち上げる。

$ cd backend/cmd
$ go run main.go

2. 認証

今回は email, passwordの認証を使います。

// access tokenを 取得
   final idTokenResult = await currentUser.getIdTokenResult();
   final token = idTokenResult.token;
   setState(() {
        widgetCurrentToken = token!;
    });
....
//  サーバー側にCustomTokenを要求
  final response = await http.post(
  Uri.parse('http://10.0.2.2:8080/getCustomToken'), //android emulator 想定
  headers: {
  'Content-Type': 'application/json',
  'AuthToken': token!,
},);

結果

こんな感じで アクセストークンとカスタムトークンが 取得できたら成功です。

I/flutter ( 6104): │ 🐛 accessToken: eyJhbGciOiJSUzI1NiIsImtpZCI6IjBj
....
I/flutter ( 6104): │ 🐛 customToken: {"customToken":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpX

3. サーバー側でアクセストークンを認証してカスタムトークンを取得して結果を返す

Firebaseサービスアカウントの秘密鍵ファイル(JSONファイル)を指定してFirebaseアプリを初期化する

func firebaseApp(ctx context.Context) (*firebase.App, error) {
	opt := option.WithCredentialsFile(os.Getenv("FIREBASE_ACCOUNT_KEY_LOCATION"))
    app, err := firebase.NewApp(ctx, nil, opt)

カスタムトークンの作成

func generateCustomToken(authClient *auth.Client, accessToken string) (string, error) {
	ctx := context.Background()
   // アクセストークンの検証
	token, err := authClient.VerifyIDToken(ctx, accessToken)
	if err != nil {
		return "", err
	}
	uid := token.UID
   // カスタムトークンの取得
	customToken, err := authClient.CustomToken(ctx, uid)
	if err != nil {
		return "", err
	}
	return customToken, nil
}

3. APIアクセスを実行してみよう

headerにアクセストークンを含めるとサーバーからレスポンスが帰ってくるはずです。
含めないと認証エラーが出るはずです。

   final response = await http.get(
   Uri.parse("http://10.0.2.2:8080/example"),
     headers: {
    'Content-Type': 'application/json',
    'AuthToken': widgetCurrentToken,
  },
);

結果

きちんとアクセストークンををheadersに含めた場合は、サーバー側で認証がとおり結果がかえってきます!

I/flutter ( 6104): │ 🐛 {"message":"Hello, World!"}

逆にアクセストークンを含めない場合は、以下のようになります。

I/flutter ( 6104): │ ⛔ {"code":401,"message":"Unauthorized"}

4. アクセストークンの有効期限がきれたら Reflesh Tokenを活用しよう

以下のように カスタムトークン新しいアクセストークンをfirebaseの認証サーバから取得します。 flutterの場合、認証情報はセキュリティ要件が厳しい場合ので、セキュアストレージを活用しましょう。

onPressed: () async {
  final Uri signInUrl = Uri.parse(
      'https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${dotenv.get('API_KEY')}');

  // カスタムトークンをbodyに含め reflesh tokenを取得する
  final refleshTokenData = await http.post(
    signInUrl,
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({
      'token': widgetCustomToken,
      'returnSecureToken': true,
    }),
  );

  if (refleshTokenData.statusCode == 200) {
    final result = jsonDecode(refleshTokenData.body);
    logger.d("refreshToken: ${result['refreshToken']}");

    // refreshTokenを使用してIDトークンを再取得
    final Uri getNewTokenUrl = Uri.parse(
        'https://securetoken.googleapis.com/v1/token?key=${dotenv.get('API_KEY')}');

    final reult = await http.post(
      getNewTokenUrl,
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({
        'grant_type': 'refresh_token',
        'refresh_token': result['refreshToken'],
      }),
    );
    if (reult.statusCode == 200) {
      final responseData = jsonDecode(reult.body);
      logger.d("IDトークン: ${responseData['id_token']}");
      setState(() {
      // アクセストークンの更新
        widgetCurrentToken = responseData["id_token"];
      });
    }
  } else {
    print('Failed to sign in with Custom Token.');
  }

結果

リフレッシュトークンをまず、認証サーバから受け取る。

I/flutter ( 6104): │ 🐛 refreshToken: AMf-vB...

新しいアクセストークンを取得。

I/flutter ( 6104): │ 🐛 newAccesesToken: eyJhbGciOiJSUzI....

以上となります。アクセストークンは1時間で切れますので、保存したリフレッシュトークンで更新する処理を repository モジュールなどで組む必要がありそうですね!

追記

こちらでgRPCの通信の詳しい解説をしました。
よければぜひ!
https://zenn.dev/renren0112/articles/c469ebfecad5a4

次回

これらの認証周りのセキュリティ要件は厳しい場合が多いと思いますので保存には、セキュアストレージを利用しましょう。

今回はRESTで実装しましたが、 gRPCサーバで同じように実装する記事を書いていきたいと思います!!

Discussion

monomono

なので、おおまかには、データが改善されていないことを確認し
認証情報を適切に管理する。といった理解でとりあえずokです。

「改竄」ですね。

renren

monoさん!いつも参考にさせていただいております!
すぐに直させていただきます!ありがとうございます!!!