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)
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
Yea sure thing! ill dig into it, and write up a post about it 😀
@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
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.
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 👍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!
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:
If I use firebase it works, but if I use api-key it says:
{
"message": "Jwt is missing",
"code": 401
}
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
@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...
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?
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
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?
@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.
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?
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.
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).