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.
- Go on Google App Engine (Go version 1.11)
- Firebase Auth
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)
})
}