.../articles/

GAE/Go + Firebase Auth

GAE/GoとFirebase Authで認証付きAPIを実装してみました。

個人的に今までパブリッククラウドはほぼAWS一択でGCPはちょっと触ったことある程度でしたが、今回新しいプロジェクトでFirebaseを使うことになったので諸々GCPに寄せてみることにしました。

APIサーバーはGo on Google App Engineで、アプリケーションからのAPIリクエストをFirebaseのIDトークンで認証するという構成です。

GoやGCP(GAE, Firebase)の作法については手探りの部分が多く試行錯誤は続けていますが、最初にまとめてしまうと現状ベースとしてこんなもんかなというのが以下のコード。

// main.go
package main

import (
	"log"
	"net/http"
	"strings"

	firebase "firebase.google.com/go"
	"github.com/go-chi/chi"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/option"
	"google.golang.org/appengine"
)

func main() {
	http.Handle("/", router())
	appengine.Main()
}

func router() http.Handler {
	r := chi.NewRouter()

	// protected routes
	r.Group(func(r chi.Router) {
		r.Use(verifyFirebaseToken)

		r.Get("/private", func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			_, err := w.Write([]byte(`{"message": "now you see private"}`))
			if err != nil {
				http.Error(w, http.StatusText(500), 500)
				return
			}
		})
	})

	// public routes
	r.Group(func(r chi.Router) {
		r.Get("/", func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			_, err := w.Write([]byte(`{"message": "now you see public"}`))
			if err != nil {
				http.Error(w, http.StatusText(500), 500)
				return
			}
		})
	})

	return r
}

func verifyFirebaseToken(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := appengine.NewContext(r)
		creds, err := google.FindDefaultCredentials(ctx)
		if err != nil {
			log.Printf("error: %v\n", err)
			http.Error(w, http.StatusText(500), 500)
			return
		}
		opt := option.WithCredentials(creds)
		app, err := firebase.NewApp(ctx, nil, opt)
		if err != nil {
			log.Printf("error: %v\n", err)
			http.Error(w, http.StatusText(500), 500)
			return
		}
		auth, err := app.Auth(ctx)
		if err != nil {
			log.Printf("error: %v\n", err)
			http.Error(w, http.StatusText(500), 500)
			return
		}
		authHeader := r.Header.Get("Authorization")
		idToken := strings.Replace(authHeader, "Bearer ", "", 1)
		token, err := auth.VerifyIDToken(ctx, idToken)
		if err != nil {
			log.Printf("error: %v\n", err)
			http.Error(w, http.StatusText(401), 401)
			return
		}
		log.Printf("token: %v\n", token)
		next.ServeHTTP(w, r)
	})
}

認証の実装について

今回はルーティングやミドルウェアの実装が簡単そう、かつあまり大げさなものでないのが良さそうだったのでchiというライブラリを使ってみました。

ポイントとしては、認証が必要なルーティングのグループで r.Use(verifyFirebaseToken) としていること。

	// protected routes
	r.Group(func(r chi.Router) {
		// FirebaseのIDトークンを検証するミドルウェア
		r.Use(verifyFirebaseToken)

		r.Get("/private", func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			_, err := w.Write([]byte(`{"message": "now you see private"}`))
			if err != nil {
				http.Error(w, http.StatusText(500), 500)
				return
			}
		})
	})

ここでグルーピングされているパスへのリクエストはFirebaseのIDトークンを検証するミドルウェアを使うことで、正しいFirebaseのIDトークンを持っていないとリクエストが届かないようになります。

func verifyFirebaseToken(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := appengine.NewContext(r)
		creds, err := google.FindDefaultCredentials(ctx)
		if err != nil {
			log.Printf("error: %v\n", err)
			http.Error(w, http.StatusText(500), 500)
			return
		}
		opt := option.WithCredentials(creds)
		app, err := firebase.NewApp(ctx, nil, opt)
		if err != nil {
			log.Printf("error: %v\n", err)
			http.Error(w, http.StatusText(500), 500)
			return
		}
		auth, err := app.Auth(ctx)
		if err != nil {
			log.Printf("error: %v\n", err)
			http.Error(w, http.StatusText(500), 500)
			return
		}
		authHeader := r.Header.Get("Authorization")
		idToken := strings.Replace(authHeader, "Bearer ", "", 1)
		token, err := auth.VerifyIDToken(ctx, idToken)
		if err != nil {
			// 正しいIDトークンがなかった
			log.Printf("error: %v\n", err)
			http.Error(w, http.StatusText(401), 401)
			return
		}
		log.Printf("token: %v\n", token)
		next.ServeHTTP(w, r)
	})
}

この例では / へのリクエストはIDトークンがなくても可能ですが、 /private へのリクエストは正しいIDトークンがないと401となり認証付きAPIが実装できました。

コードのみを示しましたが開発環境の構築やデプロイの方法、CIの活用についても色々試しているので別でまとめようと思っています。 → 続き


追記

GAE/Goでバージョン1.11に移行するにあたって google.golang.org/appengine の使用は推奨されていないようなので、一部コードを書き換えました。

情報はあまり多くないですが、サンプルコードも参考にしました。

以下書き換えた箇所を抜粋、ベースが少ないので少しだけですが。

// appengine.Main()を使わない
func main() {
	http.Handle("/", router())

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
		log.Printf("Defaulting to port %s", port)
	}

	log.Printf("Listening on port %s", port)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}
func verifyFirebaseToken(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// requestからContextを取得
		ctx := r.Context()
		creds, err := google.FindDefaultCredentials(ctx)
		if err != nil {
			log.Printf("error: %v\n", err)
			http.Error(w, http.StatusText(500), 500)
			return
		}
		opt := option.WithCredentials(creds)
		app, err := firebase.NewApp(ctx, nil, opt)
		if err != nil {
			log.Printf("error: %v\n", err)
			http.Error(w, http.StatusText(500), 500)
			return
		}
		auth, err := app.Auth(ctx)
		if err != nil {
			log.Printf("error: %v\n", err)
			http.Error(w, http.StatusText(500), 500)
			return
		}
		authHeader := r.Header.Get("Authorization")
		idToken := strings.Replace(authHeader, "Bearer ", "", 1)
		token, err := auth.VerifyIDToken(ctx, idToken)
		if err != nil {
			log.Printf("error: %v\n", err)
			http.Error(w, http.StatusText(401), 401)
			return
		}
		log.Printf("token: %v\n", token)
		next.ServeHTTP(w, r)
	})
}
.../articles/

Articles

記事

AWS AmplifyにmonorepoのNext.js(App Router)をデプロイする

AWS AmplifyにmonorepoのNext.js(App Router)をデプロイする

monorepo管理しているNext.jsをAmplifyにデプロイしようとした際にいくつか躓く内容があったのでまとめておきます。

リモートワーク・オンライン会議でも、スムーズに制作を進めるために大切なこと[資料編]

リモートワーク・オンライン会議でも、スムーズに制作を進めるために大切なこと[資料編]

コロナ禍の影響により、リモートワークの導入をおこなっている制作会社も多く、実際に弊社でも導入しています。

売れるECサイトデザインを作るために。参考にしたいおしゃれな事例の探し方。

売れるECサイトデザインを作るために。参考にしたいおしゃれな事例の探し方。

売れるECサイトのデザインは、「この形式」という決まりはありません。ECサイトで売り上げを上げるなら、しっかりとしたコンセプトと、コンセプトを決定するまでのリサーチが必要です。

制作会社の考える、業務効率化ツールのおすすめ。個人でも使いやすいサービスなど。

制作会社の考える、業務効率化ツールのおすすめ。個人でも使いやすいサービスなど。

新型コロナウイルス感染拡大の影響で、リモートワークが主流になり、弊社でも週のほとんどは各自宅で作業をしています。

Figmaでデザインのコミット履歴を残せるプラグイン【Thought Recorder】をリリースしました

Figmaでデザインのコミット履歴を残せるプラグイン【Thought Recorder】をリリースしました

Figmaを利用するWebデザイナーの助けになれると嬉しいです。使い方は本記事をご覧ください。

ECの構築方法、おすすめのECサービス。

ECの構築方法、おすすめのECサービス。

ファッションや家電、スーパーの買い物でさえもECサイトを利用することが当たり前になりました。加えて新型コロナウイルスの影響もあり、弊社にも「どんなプラットフォームを利用したら良いか」「どれくらいコストがかかるのか」などECに関するさまざまなご相談を頂きます。

FastAPIのスキーマクラスをOpenAPIから生成する方法

FastAPIのスキーマクラスをOpenAPIから生成する方法

PythonでAPIを構築する要件があり、フレームワークに比較的モダンなFastAPIを採用しました。FastAPIはバックエンドの開発を行えば自動でOepnApi定義を生成する機能が備わっていますが、今回はこれを使わず、事前に用意したOepnApi定義からFastAPIで利用するスキーマクラスを生成する方法を紹介します。

Laravel 日本一解りやすい全文検索のマイグレーション記載方法解説

Laravel 日本一解りやすい全文検索のマイグレーション記載方法解説

Laravel + MySQLで全文検索を実装する

GiFT1号目新卒デザイナーの2021年振り返り

GiFT1号目新卒デザイナーの2021年振り返り

いつの間に、年末ですね。入社してもう、9ヶ月も立っていたようです。2021年の振り返りを記事にしました。

TimesclaeDBのデータ圧縮に関して

TimesclaeDBのデータ圧縮に関して

TimescaleDBはデータベース内の一部のテーブルを時系列データとして扱えるPostgreSQLの拡張です。PostgreSQLの機能拡張なので非常に手軽に導入できます。今回はこのTimesaceDBの圧縮について調べたので備忘録として書き綴りました。

すべての記事

お問い合わせ