GCP Api Gateway: Firebase Authentication

amammay

Alex Mammay

Posted on September 23, 2020

GCP Api Gateway: Firebase Authentication

Google Cloud Platform Api Gateway

github repo --> here

What is API Gateway?

As per the documentation, Api gateway is a fully managed gateway for serverless workloads.

So really at the end of the day, that boils down to a serverless gateway for you serverless api's... Man that's alot of serverless.

What does API Gateway do?

API gateway will act as the middleman between an end user and your services. You describe your services according to the OpenAPI specification,
upload the specification to api gateway center and then finally deploy the spec to a gateway. API Gateway also provides a suite of utilities, such as monitoring, logging and authentication.

When should I use API Gateway?

If you are going to be using the serverless GCP eco-system(Cloud Functions, Cloud Run, App Engine), then API Gateway will be complimentary to those serverless products.

Why should I use API Gateway?

The most important question to answer is "why", why should I use API Gateway?

Security - Your core application can be deployed and protected by GCP IAM, that way the only direct interactions with your services will be done with the api gateway.

Externalized Configuration - You have an external way to manage application authentication, service url mapping, and API documentation that lives outside the context of the application.

Less Code - Your application itself won't have to worry about validating JWT/Api Keys since those will be handled at the gateway layer, and the result will be forwarded to your application. The less code you write, the fewer bugs there will be 😬

Observability - All your performance metrics will roll up to a single, easy to view dashboard with all your KPI's such as request latency, error rates, requests per second and more.

Securing Cloud Run Services with Firebase Authentication.

First we will take a look at our openapi specification file to get an understanding of our api. It is pretty straight forward, a single endpoint named /greet that will echo back the users name but in a "greeted" format.

openapi2-run.yaml

swagger: '2.0'
info:
  # Title of the api gateway
  title: gateway
  description: Sample API on API Gateway with a Cloud Run backend
  version: 1.0.0
schemes:
  - https
produces:
  - application/json
# The cloud run service url, this could also be defined per path as well in case you have multiple cloud run services that
# make up a single gateway
x-google-backend:
  address: "YOUR-CLOUD_RUN-URL"
securityDefinitions:
  firebase:
    authorizationUrl: ""
    flow: "implicit"
    type: "oauth2"
    # Replace YOUR-PROJECT-ID with your project ID
    x-google-issuer: "https://securetoken.google.com/YOUR-PROJECT-ID"
    x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken@system.gserviceaccount.com"
    x-google-audiences: "YOUR-PROJECT-ID"
paths:
  /greet:
    get:
      summary: Greets the user
      operationId: greet
      # define that our service uses the firebase security definition
      security:
        - firebase: [ ]
      responses:
        '200':
          description: A successful response
          schema:
            type: object
            # our object look like `{name: ""}`
            properties:
              name:
                type: string
                description: The users name

Now we will take a peek at the code that will fulfill the contract of the api.

cmd/routes.go

func (s *server) routes() {
    s.router.HandleFunc("/greet", s.handleAuth(s.handleGreeting()))
}

//handleGreeting will fetch the UserInfo struct that is stored in context from our auth middleware and use that to greet the person that called our api
func (s *server) handleGreeting() http.HandlerFunc {

    type person struct {
        Name string `json:"name"`
    }

    return func(writer http.ResponseWriter, request *http.Request) {

        writer.Header().Set("Content-Type", "application/json")

        // fetch the token user object that is stored in context
        userObj := request.Context().Value(gatewayUserContext).(UserInfo)

        // greet the user 👋
        p := person{Name: fmt.Sprintf("Hello 👋 %s", userObj.Name)}
        decoder := json.NewEncoder(writer)

        if err := decoder.Encode(p); err != nil {
            http.Error(writer, err.Error(), http.StatusInternalServerError)
        }
    }
}

And just for a quick review of the Auth Middleware.

cmd/auth.go

const gatewayUserInfoHeader = "X-Apigateway-Api-Userinfo"
const gatewayUserContext = "GATEWAY_USER"

type UserInfo struct {
    Name          string   `json:"name"`
    Picture       string   `json:"picture"`
    Iss           string   `json:"iss"`
    Aud           string   `json:"aud"`
    AuthTime      int      `json:"auth_time"`
    UserID        string   `json:"user_id"`
    Sub           string   `json:"sub"`
    Iat           int      `json:"iat"`
    Exp           int      `json:"exp"`
    Email         string   `json:"email"`
    EmailVerified bool     `json:"email_verified"`
    Firebase      Firebase `json:"firebase"`
}
type Identities struct {
    GoogleCom []string `json:"google.com"`
    Email     []string `json:"email"`
}
type Firebase struct {
    Identities     Identities `json:"identities"`
    SignInProvider string     `json:"sign_in_provider"`
}

// handleAuth is a piece of middleware that will parse the gatewayUserInfoHeader from the request and add the UserInfo to the request context
func (s *server) handleAuth(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {

        encodedUser := r.Header.Get(gatewayUserInfoHeader)
        if encodedUser == "" {
            http.Error(w, "User Not Available", http.StatusForbidden)
            return
        }
        decodedBytes, err := base64.RawURLEncoding.DecodeString(encodedUser)
        if err != nil {
            http.Error(w, "Invalid UserInfo", http.StatusForbidden)
            return
        }
        decoder := json.NewDecoder(bytes.NewReader(decodedBytes))
        var userToken UserInfo
        err = decoder.Decode(&userToken)
        if err != nil {
            http.Error(w, "Invalid UserInfo", http.StatusForbidden)
            return
        }

        ctx := context.WithValue(r.Context(), gatewayUserContext, userToken)
        h.ServeHTTP(w, r.WithContext(ctx))

    }
}

You can see the rest of the code for the application in the github repo.

Enable API's and Create service account

Make sure you enable the following api's on your project

gcloud services enable apigateway.googleapis.com
gcloud services enable servicemanagement.googleapis.com
gcloud services enable servicecontrol.googleapis.com

Lets create our api gateway service account

gcloud iam service-accounts create api-gateway

gcloud projects add-iam-policy-binding YOUR_PROJECT_ID --member "serviceAccount:api-gateway@YOUR_PROJECT_ID.iam.gserviceaccount.com" --role "roles/run.invoker"
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID --member "serviceAccount:api-gateway@YOUR_PROJECT_ID.iam.gserviceaccount.com" --role "roles/iam.serviceAccountUser"

Build and Deployment

Since we are going to be using cloud build to roll everthing out, lets get our container built and deployed with gcloud builds submit

cloudbuild.yaml

steps:

  # Run the docker build
  - name: 'gcr.io/cloud-builders/docker'
    args: [ 'build', '-t', 'gcr.io/$PROJECT_ID/greeter', '.' ]

  # push the docker image to the private GCR registry
  - name: 'gcr.io/cloud-builders/docker'
    args: [ 'push', 'gcr.io/$PROJECT_ID/greeter' ]

  # deploy to cloud run
  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
    entrypoint: gcloud
    args: [ 'run', 'deploy', 'greeter', '--image', 'gcr.io/$PROJECT_ID/greeter', '--region', 'us-central1', '--platform', 'managed', '--no-allow-unauthenticated' ]

images:
  - 'gcr.io/$PROJECT_ID/greeter'

Now that we have the cloud service deployed, we just need to get our cloud run url gcloud run services describe greeter --format 'value(status.url)'
and to verify its secure we can try to curl the endpoint curl $(gcloud run services describe greeter --format 'value(status.url)') and we should get a 403.

The next step is to take that url from our endpoint and plug it into our api spec as the address for the x-google-backend

# The cloud run service url, this could also be defined per path as well in case you have multiple cloud run services that
# make up a single gateway
x-google-backend:
  address: "YOUR-CLOUD_RUN-URL"

Now we are turning to the home stretch, we just need to deploy our gateway/config


# create api config
gcloud beta api-gateway api-configs create echoconf \
  --api=gateway --openapi-spec=openapi2-run.yaml \
  --backend-auth-service-account=api-gateway@YOUR_PROJECT_ID.iam.gserviceaccount.com


# create gateway with config
gcloud beta api-gateway gateways create gateway \
  --api=gateway --api-config=echoconf \
  --location=us-central1


#get hostname from gateway
gcloud beta api-gateway gateways describe gateway \
  --location=us-central1 --format 'value(defaultHostname)'

Now if we curl the gateway endpoint with

curl "https://$(gcloud beta api-gateway gateways describe gateway --location=us-central1 --format 'value(defaultHostname)')/greet"

we will get a 401 since we didn't attach a firebase identity token to request.

But once you attach the token to the request as either a query param of access_token or as an Authorization header with bearer token

curl "https://$(gcloud beta api-gateway gateways describe gateway --location=us-central1 --format 'value(defaultHostname)')/greet?access_token=ACCESS_TOKEN"

We will get back our greeted message! {
"name": "Hello 👋 Alex Mammay"
}

💖 💪 🙅 🚩
amammay
Alex Mammay

Posted on September 23, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Google Tools that you should use
googlecloud Google Tools that you should use

September 20, 2021

GCP Api Gateway: Firebase Authentication
googlecloud GCP Api Gateway: Firebase Authentication

September 23, 2020