.../articles/
GAE/Go + Firebase Auth

GAE/Go + Firebase Auth

2019.03.17

I've personally stuck almost exclusively with AWS for public cloud and had only touched GCP a little, but since a new project called for using Firebase, I decided to move things over to GCP.

The setup is a Go API server on Google App Engine, authenticating API requests from the application using Firebase ID tokens.

There's still a lot of trial and error around the conventions for Go and GCP (GAE, Firebase), but here's the code that represents roughly where things stand as a starting point.

// 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)
	})
}

About the authentication implementation

For this project I used a library called chi, since it looked like it would keep the routing and middleware implementation simple without being overly heavyweight.

The key point is that r.Use(verifyFirebaseToken) is applied to the group of routes that require authentication.

	// protected routes
	r.Group(func(r chi.Router) {
		// middleware that verifies the Firebase ID token
		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
			}
		})
	})

By using this middleware that verifies the Firebase ID token, requests to paths grouped here will only go through if they carry a valid Firebase ID token.

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 {
			// no valid ID token was present
			log.Printf("error: %v\n", err)
			http.Error(w, http.StatusText(401), 401)
			return
		}
		log.Printf("token: %v\n", token)
		next.ServeHTTP(w, r)
	})
}

In this example, requests to / are allowed without an ID token, while requests to /private return a 401 without a valid ID token — giving us an authenticated API.

I've only shown the code here, but I've also been experimenting with setting up the development environment, deployment, and CI, which I plan to cover separately. → Continued here


Addendum

When migrating GAE/Go to version 1.11, using google.golang.org/appengine appears to no longer be recommended, so I rewrote part of the code.

There isn't a lot of information out there, but I also referenced the sample code.

Below are excerpts of the parts I rewrote — there isn't much of a base to work from, so it's just a small amount.

// don't use 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) {
		// get the Context from the request
		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)
	})
}

written by

.../article/

Articles

All articles

From Firebase to Vercel, Contentful to microCMS — a migration log written with Claude Code

From Firebase to Vercel, Contentful to microCMS — a migration log written with Claude Code

We moved our corporate site's hosting and CMS, and made it bilingual along the way. The constraints we only found by running against real data were more useful than the migration itself, so this post focuses on where we got stuck.

Can't Read POST Data with Firebase Functions × Remix?

Can't Read POST Data with Firebase Functions × Remix?

How to read POST data from a Remix action when running on Firebase Functions.

Generative AI for Executives and Leaders: An Approach to Self-Driven DX

Generative AI for Executives and Leaders: An Approach to Self-Driven DX

Building a structure where executives and leaders themselves can identify issues and evaluate solutions using generative AI. We introduce how combining this with our hands-on support dramatically improves both the quality and speed of digital transformation.

Deploying a Monorepo Next.js App (App Router) to AWS Amplify

Deploying a Monorepo Next.js App (App Router) to AWS Amplify

Notes on the obstacles we hit while deploying a Next.js app managed in a monorepo to AWS Amplify.

Keeping Production Running Smoothly with Remote Work and Online Meetings [Documentation]

Keeping Production Running Smoothly with Remote Work and Online Meetings [Documentation]

Many production companies have adopted remote work as a result of the pandemic, and we are one of them.

Designing an E-Commerce Site That Sells: How to Find Great Reference Examples

Designing an E-Commerce Site That Sells: How to Find Great Reference Examples

There is no single formula for e-commerce design that sells. Driving revenue requires a solid concept, and getting to that concept requires thorough research.

Productivity Tools We Recommend as a Production Company, Including Services That Work Well Solo

Productivity Tools We Recommend as a Production Company, Including Services That Work Well Solo

With remote work becoming the norm during the COVID-19 pandemic, our team now works from home most days of the week.

We Released Thought Recorder, a Figma Plugin for Keeping a Commit History of Your Designs

We Released Thought Recorder, a Figma Plugin for Keeping a Commit History of Your Designs

We hope this helps web designers who work in Figma. Read on for how to use it.

How to Build an E-Commerce Site, and Which Platforms We Recommend

How to Build an E-Commerce Site, and Which Platforms We Recommend

Shopping online for fashion, appliances, and even groceries is now routine. With the pandemic accelerating the shift, we receive a steady stream of questions about which platform to use and how much it costs.

Generating FastAPI Schema Classes from OpenAPI

Generating FastAPI Schema Classes from OpenAPI

We chose FastAPI, a relatively modern framework, for a Python API project. FastAPI can generate an OpenAPI definition from your backend code, but here we do the opposite: generating FastAPI schema classes from an OpenAPI definition prepared in advance.

View all articles

Contact us