DEV Community

Alex Mammay
Alex Mammay

Posted on • Edited on

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"
}

Top comments (16)

Collapse
 
rhernand3z profile image
Rafael H

Great article @amammay ,

I am trying to accomplish something similar but with an API key approach. I followed similar steps and changed the openapi yml to use API key as the security definition. Unfortunately, I keep getting 403 errors even after I created an API key with no restrictions. Would you have some tips when integrating API Gateway with API keys on Cloud Run? Appreciate your help

Collapse
 
amammay profile image
Alex Mammay

Yea sure thing! ill dig into it, and write up a post about it 😀

Collapse
 
amammay profile image
Alex Mammay

@rhernand3z after doing a bit of digging the main thing i could see you having problems with api key auth is to make sure you have this in your security definition

securityDefinitions:
  # This section configures basic authentication with an API key.
  api_key:
    type: "apiKey"
    name: "key"
    in: "query"
Enter fullscreen mode Exit fullscreen mode

and in addition to create the api key entry for

API_ID specifies the name of your API.
PROJECT_ID specifies the name of your Google Cloud project.

gcloud services enable API_ID.apigateway.PROJECT_ID.cloud.goog

i believe you still need to run that command even if your api key is set to unrestricted to create the entry in GCP behind the scenes.

Thread Thread
 
rhernand3z profile image
Rafael H

Hey Alex,

I'll give this a shot, I didn't execute the last step via gcloud services enable..., might have been the culprit. Thanks for digging into this 👍

Thread Thread
 
rhernand3z profile image
Rafael H

Hey @amammay -- I was able to get it working properly. You were right the culprit was not executing:

gcloud services enable ...

Thanks for your help and a great article!

Collapse
 
evanegasveredata profile image
Ernesto Vanegas • Edited

Hi Alex @amammay ! Thank you for the article! It's good to see that the google product is starting to get traction because there's still missing some documentation around it. I have a case where I am trying to use two security methods: api-key OR firebase. But whenever I place the firebase authentication, it only works with that one. Any pointers to solve this?


securityDefinitions:
api_key:
type: "apiKey"
name: "x-api-key"
in: "header"
firebase:
authorizationUrl: ""
flow: "implicit"
type: "oauth2"
# Replace YOUR-PROJECT-ID with your project ID
x-google-issuer: "securetoken.google.com/YOUR-PROJEC..."
x-google-jwks_uri: "googleapis.com/service_accounts/v1..."
x-google-audiences: "YOUR-PROJECT-ID"
schemes:

  • https produces:
  • application/json paths: /scoring: post: summary: Score operationId: score-v1 security: - api_key: [] - firebase: [] x-google-backend: address: MYBACKEND responses: '200': description: OK '401': description: Not authorized

If I use firebase it works, but if I use api-key it says:

{
"message": "Jwt is missing",
"code": 401
}

Collapse
 
amammay profile image
Alex Mammay

hmmm that is quite interesting, it seems as your swagger definition looks good. Ill have to give it a try and see if it produces similar results

Collapse
 
evanegasveredata profile image
Ernesto Vanegas

@amammay So... i've tried recreating the gateway, changing different alternatives of security order but nothing. I've created a bug in their issue tracker but I think it will take some time to fix...
issuetracker.google.com/issues/186...

Collapse
 
phillduffy profile image
Phill Duffy

There doesn't seem to be a lot of help around this online, thank you for your post.

I have a question around verifying the token, I am using NodeJS.

I am able to get the User information out of 'X-Apigateway-Api-Userinfo' - I am not sure if I need to use the Admin SDK to verify this Token, or whether I now just pull out the information, like you do, and trust the token has been verified - is that right?

Collapse
 
amammay profile image
Alex Mammay

Yea, you should be able to pull the base64 encoded token straight from X-Apigateway-Api-Userinfo. Once you grab the header you should be to do base64 decode on it and it will have the correct json stucture

Collapse
 
techd1984 profile image
techd1984

Hi Alex, Nice article @amammay

I've a cloud run in private mode with only authenticated users enabled, I'm not sure how'd I authenticate with firebase and where do you get the token from? As per other documents it looks like you need another layer for authentication and this model won't work in case cloud run is private and no-allow-unauthenticated and since cloud run supports IAM what'd be the use of this in that case?

Collapse
 
amammay profile image
Alex Mammay

@techd1984 sorry for getting back to you late, but the overall flow is like so

your web app (managing your users with the firebase js sdk for them to sign in etc.) get their firebase auth token --- http call with auth in header ---> api gateway (api gateway contains the auth definition to say to use firebase auth to verify access to the endpoint specified in the yaml file --- api gateway proxies request to your cloud run endpoint using service account credentials ---> your private cloud run endpoint.

this allows you to make sure native GCP iam is used to access the raw cloud run url, and only a subset of your endpoints is exposed to your users with firebase auth. As for auth with firebase... check out this video to get some more context around firebase youtube.com/watch?v=9kRgVxULbag at the end of the day you would just be calling api gateway with your end users tokens.

Collapse
 
andrefedev profile image
Andres Osorio

I have a question, how can we change the url of the gateway api for our custom domain. And what is the difference with Google Cloud Endpoints?

Collapse
 
amammay profile image
Alex Mammay
  1. So currently you cant assign a custom domain to api gateway yet, i would imagine some point in the future you could register a custom domain to it. (either via some native control or at a GCLB level to map as a Serverless NEG)
  2. The difference between this and cloud endpoints is that this is a managed service that you don't have to worry about running the underlying infrastructure to.
Collapse
 
chengchinlim profile image
Cheng Chin Lim

Hi, if the requests are unauthenticated, will they still be passed to our services (App Engine)? I am afraid of getting a huge bill due to DDOS attack. Thank you.

Collapse
 
amammay profile image
Alex Mammay

The requests will not be forwarded to the target endpoint if the auth is missing (as long as you have auth strictly defined within your open api spec).