gcp

Issuing Service Account Self-Signed JWTs on AppEngine, GCE, Cloud Run and Cloud Functions

2022-04-22

So…how do you get an AppEngine app to use its service account to issue a sign some data or generate JWT?

AppEngine, Compute Engine, Cloud Run, Cloud Functions, all provide you an easy way to give you an id_token to access services on GCP you deploy that accept those type of tokens.

But how would an appengine instance or any of the other platforms to sign a JWT or sign any arbitrary data with its own service account? For example you need to access a Cloud Endpoints application which has Service Account JWT authentication?

Well, you can issue a private key for the appengine service account and then import that key as a GCP Secret…but thats a not a good idea (you should avoid downloading service account keys!)

The better idea is to somehow sign some data without a key.

how? use the iamcredentials.signBlob(), iamcredentials.signJwt() API and service account impersonation.

Basically, you’re allowing the appengine app to access its private key via API to do stuff like sign stuff.

For details, see

appengine->jwt->endpoints

What this tutorial shows is how to setup an AppEngine Application, configure “self-impersonation” which will allow it to signJWT or signBlob using its own service account.

You can use this same technique to sign JWTs or signBlobs for the service account assigned to Compute Engine, GKE, Cloud Run, Cloud Functions too.


Some other references


Setup

First configure a google project which as appengine enabled (note you can apply much of this to Cloud Run, GCF or GCE instances)

export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format='value(projectNumber)'`

# allow the default service account to 'self ipersonate'
gcloud iam service-accounts add-iam-policy-binding  \
       $PROJECT_ID@appspot.gserviceaccount.com  \
       --member serviceAccount:$PROJECT_ID@appspot.gserviceaccount.com \
       --role roles/iam.serviceAccountTokenCreator

# download the various files shown below into its own directory
# within that directory, install the dependencies into the lib/ folder
pip install -r requirements.txt -t lib

# now deploy the app (this will create a service called py-frontend)
gcloud app deploy --version=1 --no-promote --version 1

# tail the appengine logs
gcloud app logs tail -s py-frontend

In a new window, access the service

curl -s https://1-dot-py-frontend-dot-$PROJECT_ID.appspot.com

What you should see is several things some of which are just to baseline and to demo

First we just show details about the access_token the appengine service runs as:

2022-04-24 12:28:50 py-frontend[1]  {'id': '107145139691231222712', 'email': 'mineral-minutia-820@appspot.gserviceaccount.com', 'verified_email': True, 'picture': 'https://lh3.googleusercontent.com/a/default-user=s96-c'}

Then we show an id_token for the appengine service

2022-04-24 12:28:50 py-frontend[1]  {'args': {}, 'headers': {'Accept': 'application/json', 'Accept-Encoding': 'gzip, deflate', 'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImQzMzJhYjU0NWNjMTg5ZGYxMzNlZmRkYjNhNmM0MDJlYmY0ODlhYzIiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL3lvdXJfYXVkaWVuY2UiLCJhenAiOiIxMDcxNDUxMzk2OTEyMzEyMjI3MTIiLCJlbWFpbCI6Im1pbmVyYWwtbWludXRpYS04MjBAYXBwc3BvdC5nc2VydmljZWFjY291bnQuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTY1MDgwNjkzMCwiaWF0IjoxNjUwODAzMzMwLCJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMDcxNDUxMzk2OTEyMzEyMjI3MTIifQ.HkGtmciFatrBNM5WyhLxQsM25pI2y_GCO4upjv05oZo5m5K3PY-vF8Zb3jRSA6tdhTePowN1lZpddikz8DDgudGGh1Mt54n40kFAm8dlPEojPBgwXT0ektF-Pz4xVxuPEzm4zdDwL8Ee8C5H-n39_dM2zVXHOlpWkrWTwZJOHoZrgc1nw9aLRBOf_u3MxaDnFSsWYa85Ej0QIjZ8AhD-HNQQN3pqVl8zxu-XxncbBHb3fx3DRsyb7mCHZaSctGdcWW-4BI2XVZFYbAEiGJwcYDNmtaENrlyzRSuGwRH13d358n5fIJ6SgxqDARdmAUmF_RS-ekjI0goxuS8l0Jypag', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.27.1', 'X-Amzn-Trace-Id': 'Root=1-62654282-4f0073691b8735587c1bf3b2'}, 'origin': '107.178.237.17', 'url': 'https://httpbin.org/get'}

Note the JWT issuer and claims. This is an id_token signed and issued by google

{
  "alg": "RS256",
  "kid": "d332ab545cc189df133efddb3a6c402ebf489ac2",
  "typ": "JWT"
}.
{
  "aud": "https://your_audience",
  "azp": "107145139691231222712",
  "email": "mineral-minutia-820@appspot.gserviceaccount.com",
  "email_verified": true,
  "exp": 1650806930,
  "iat": 1650803330,
  "iss": "https://accounts.google.com",
  "sub": "107145139691231222712"
}

Finally, what we were actually after: a self-signed JWT

2022-04-24 12:28:51 py-frontend[1]  {'args': {}, 'headers': {'Accept': 'application/json', 'Accept-Encoding': 'gzip, deflate', 'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjEzYTc0Njc3ZTI2YTcxNTkxZWE3ZTE1YjMyNGE0ZWIxYWU2ZmM2M2QiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiAibWluZXJhbC1taW51dGlhLTgyMEBhcHBzcG90LmdzZXJ2aWNlYWNjb3VudC5jb20iLCAiZW1haWwiOiAibWluZXJhbC1taW51dGlhLTgyMEBhcHBzcG90LmdzZXJ2aWNlYWNjb3VudC5jb20iLCAiYXVkIjogImh0dHBzOi8veW91cl9hdWRpZW5jZSIsICJzdWIiOiAibWluZXJhbC1taW51dGlhLTgyMEBhcHBzcG90LmdzZXJ2aWNlYWNjb3VudC5jb20iLCAiZXhwIjogMTY1MDgwMzMzMiwgImlhdCI6IDE2NTA4MDMzMzB9.uvAyRE9eMePUuajkOT4vNN-FKRg-dVUIAKb1G_7rtfE1W6S4p_QqMApoos6OjENZ456zEnrYwz7CiMbvkhhdwT7tbge1FZaQRpf1RZJwK7qaP6P2o9wYuuh8388bLYtTTLqf8739F5FiL3QRkr6HFx1HzoG0BgCrW9WxjqDJyy6Ff-NAglQjvD8OPqe0a-nx2RKyj3-cjigdzZWJHPlVmAibn4T5yHrurjsnUGJGCuQ8SZVVKWEpBB-K1AH3-fdHG0TkRtGLLyA4i9mhtcQGJB_SXmvhajGmSR8WiHwI14Of412nInB7nfRx3RBFAxQpgH7O9gy-ap11edbaGv4djw', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.27.1', 'X-Amzn-Trace-Id': 'Root=1-62654282-47a877d07b375cf30ef7d84d'}, 'origin': '107.178.237.17', 'url': 'https://httpbin.org/get'}

Notice the issuer and claims. This is signed and issued by our service account

{
  "alg": "RS256",
  "kid": "13a74677e26a71591ea7e15b324a4eb1ae6fc63d",
  "typ": "JWT"
}.
{
  "iss": "mineral-minutia-820@appspot.gserviceaccount.com",
  "email": "mineral-minutia-820@appspot.gserviceaccount.com",
  "aud": "https://your_audience",
  "sub": "mineral-minutia-820@appspot.gserviceaccount.com",
  "exp": 1650803332,
  "iat": 1650803330
}

Notice that the kid field denotes which private key from the list of keys this service account has that was used to sign the JWT.

Cloud Endpoints can verify this JWT by access the “verification” endpoint for the certs here

(notice the key_id is listed there 13a74677e26a71591ea7e15b324a4eb1ae6fc63d)

Anyway, what you can do is inject this jwt or id_token into an AuthorizedSession and make an api call to your app

    session = google.auth.transport.requests.AuthorizedSession(jwt_credentials)
    request_url = "https://httpbin.org/get"
    response = session.get(request_url, headers = {'Accept': 'application/json'})
    print(response.json())

Files

  • main.py
  • app.yaml
runtime: python37
service: py-frontend
#service_account: SA2@Project.iam.gserviceaccount.com
  • requirements.txt
Flask
Werkzeug
google-auth
google-cloud-iam
  • appengine_config.py:
import os
import sys
import logging

from google.appengine.ext import vendor
vendor.add('lib')
sys.path.append(os.path.join(os.path.dirname(__file__), 'lib'))

logging.basicConfig(level=logging.INFO)

This site supports webmentions. Send me a mention via this form.