This tutorial and code samples cover how customers that use Workload identity federation can authenticate to those GCP Services like Cloud Run and Cloud Functions which accept OpenID Connect (OIDC)
tokens.
Basically, this walks you through a self-contained setup where you will:
OIDC
identity “provider” that just happens to run on Cloud Run which returns fake OIDC
tokens (eg. ambient credentials). Pretend this is your on-prem OIDC system.GCP Workload Identity
with that OIDC
providerCloud Run
application which requires Authentication.Workload Federation
and IAM API
to exchange the fake OIDC token from step 1 for a Google-issued OIDC tokenid_token
from step 4 to access the secure Cloud Run application.A brief background:
This allows user with ambient credentials from Azure
, AWS
, SAML
or arbitrary OIDC
providers to authenticate to GCP Services like Compute Engine
, Google Storage
, etc without needing a local Google Credential or Service Account.
Authenticating using Google OpenID Connect Tokens
Describes how you can authenticate to Applications that you deploy on platform such as Cloud Run
or Cloud Functions
This article combines both where you can use your ambient workload credentials to authenticate to Cloud Run.
Why is this different than plain plain Workload Federation?
Well, the token returned by GCP for workload federation is an oauth2 access_token
intended to be used with GCP services
The token we actually need to access cloud run is an id_token
So how do we exchange an access_token
for an id_token
?
Well, we don’t really use an access_token
here and instead take a shortcut:
Since we need to invoke projects.serviceAccounts.generateAccessToken and that endpoint supports using federated token returned by GCP’s STS server, we can skip using an access_token.
We can use the federated token because the iamcredentials API happens to support that direct token type.
What this means is there is no need to set impersonation again by setting the service_account_impersonation_url
parameter in the ADC config file.
That IAM endpoint will generate a google-issued id_token
with the user-specified audience that matches the Cloud Run service.
gRPC
: If what your ultimately interested in is an id_token
for gRPC, follow the step to get an id_token and then supply that using gRPC Authentication with Google OpenID Connect tokens
SignedURL
If what you really want to use do is issue GCS Signed URLs using workload identity, you need to invoke projects.serviceAccounts.signBlob using the correct payload.
Ideally, customers should not have to manually invoke the signBlob() and then reconstruct the specific protocol to sign the URL. This is manual signing is described here and is certainly a pain. This capability to signURLs should be included in the various google-auth libraries. For example, the federated Credential provider in google-auth-python
is external_account.py and that construct is missing the credentials.Signing
interface (class Credentials(..., credentials.Signing):
). For golang, you can invoke a signer func inline like this. You will also need to allow the federated principal://
or principalSet://
the roles/iam.serivceAccountTokenCreator
on the service account (to allow it to signBlob()
)
Domain-wide Delegation
: If what want to do is issue a domain-wide delegated credentials (which IMO should be really rare), use projects.serviceAccounts.signJwt with the JWT payload and sub
field for the new identity.
For more info see Using Federated Tokens to sign signJWT or get gcp idtokens
You can find the source here
As mentioned, there are several preliminary steps involved here which are numbered accordingly.
You will need a GCP project and access to deploy to cloud run (ofcourse)
export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format='value(projectNumber)'`
## Create service account that the google_issued id_token will use
gcloud iam service-accounts create oidc-federated
The following will build and deploy the fake OIDC server and allow the service account associated with the federated identity to access the service
cd idp_server/
gcloud run deploy --platform=managed \
--region=us-central1 --allow-unauthenticated \
--source . idp-on-cloud-run
The fake oidc server’s
/token
endpoint will sign whatever well-formed JSON file is sent via POST
/.well-known/openid-configuration
endpoint will print out the standard oidc configuration
/certs
will return the JWK
formatted public key used for JWT
verification
gcloud run services describe idp-on-cloud-run --format="value(status.url)"
# export some environment variables
export URL=`gcloud run services describe idp-on-cloud-run --format="value(status.url)"`
export IAT=`date -u +%s`
export EXP=`date -u +%s -d "+3600 seconds"`
export EMAIL="alice@domain.com"
export SUB="alice@domain.com"
export ISS=$URL
export NAME="alice"
export WF_AUD="https://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-pool-run/providers/oidc-provider-1"
## print a sample fake id_token
curl -s -X POST -d "{ \"name\":\"$NAME\", \"isadmin\":\"true\", \"mygroups\": [ \"group1\",\"group2\"], \"iss\":\"$ISS\", \"aud\":\"$WF_AUD\", \"sub\":\"$SUB\", \"iat\":$IAT, \"exp\":$EXP, \"email\":\"$EMAIL\", \"email_verified\":true }" $URL/token > /tmp/oidccred.txt
You can view the contents of the JWT by decoding /tmp/oidccred.txt
using jwt.io debugger
A sample JWT
may look like
{
"alg": "RS256",
"kid": "123456",
"typ": "JWT"
}
{
"aud": "https://iam.googleapis.com/projects/1071284184436/locations/global/workloadIdentityPools/oidc-pool-run/providers/oidc-provider-1",
"email": "alice@domain.com",
"email_verified": true,
"exp": 1644598160,
"iat": 1644594560,
"isadmin": "true",
"iss": "https://idp-on-cloud-run-6w42z6vi3q-uc.a.run.app",
"mygroups": [
"group1",
"group2"
],
"name": "alice",
"sub": "alice@domain.com"
}
Now configure workload identity to use the fake oidc server’s JWT’s
gcloud iam workload-identity-pools create oidc-pool-run \
--location="global" \
--description="OIDC Pool" \
--display-name="OIDC Pool" \
--project $PROJECT_ID
gcloud iam workload-identity-pools providers create-oidc oidc-provider-1 \
--workload-identity-pool="oidc-pool-run" --issuer-uri="$ISS" \
--location="global" \
--attribute-mapping="google.subject=assertion.sub,attribute.isadmin=assertion.isadmin,attribute.aud=assertion.aud" \
--attribute-condition="attribute.isadmin=='true'" \
--project $PROJECT_ID
# write the GOOGLE_APPLICATION_CREDENTIAL file to /tmp/sts.creds
gcloud iam workload-identity-pools create-cred-config \
projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-pool-run/providers/oidc-provider-1 \
--output-file=/tmp/sts-creds.json \
--credential-source-file=/tmp/oidccred.txt
## Associate the federated identity (alice@domain.com) the permission to impersonate the service account
gcloud iam service-accounts add-iam-policy-binding oidc-federated@$PROJECT_ID.iam.gserviceaccount.com \
--role roles/iam.workloadIdentityUser \
--member "principal://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-pool-run/subject/alice@domain.com"
Note that the /tmp/sts-creds.json
file does NOT specify a service account since IAMCredentials API support direct federation. Also note that were instructing the credential to find the ambient credential set at /tmp/oidccred.txt
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/1071284184436/locations/global/workloadIdentityPools/oidc-pool-run/providers/oidc-provider-1",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"file": "/tmp/oidccred.txt"
}
}
If you intend to access any other GCP service like GCE or GCS within the same application where you access Cloud Run, please specify service_account_impersonation_url
parameter since those services do not support federated tokens yet and require the actual access_token
. Your can do that (by setting --service-account
to create-cred-config
command). This article focuses on just accessing cloud run and acquiring an id_token
so the abridged flow using the federated token is shown
If you’re on the cloud console, your configuration should look like
Also note that roles/iam.workloadIdentityUser
includes the following permissions:
iam.serviceAccounts.get
iam.serviceAccounts.getAccessToken
iam.serviceAccounts.getOpenIdToken
iam.serviceAccounts.list
But all you need is just the iam.serviceAccounts.getOpenIdToken
permission…which means the role is over-privleged. You may want to just instead use a custom role with the required iam.serviceAccounts.getOpenIdToken
permission. Its similar to
See Concentric IAMCredentials Permissions: The secret life of signBlob. If you just need to generate an id_token
, consider generating a custom role with that specific capability.
Cloud Run
applicationNow deploy an application that is secure. This is the app we want to access using the google id_token
cd server
gcloud run deploy --platform=managed \
--region=us-central1 \
--no-allow-unauthenticated \
--source . federated-auth-cloud-run
export TEST_URL=`gcloud run services describe federated-auth-cloud-run --format="value(status.url)"`
# verify that you can't access the service
curl -s $TEST_URL
# allow the service account we will eventually use for access to the cloud run service
gcloud run services add-iam-policy-binding federated-auth-cloud-run \
--member="serviceAccount:oidc-federated@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/run.invoker"
Workload Federation
and IAM API
to create a google id_tokenGCP Auth libraries automatically ‘understand’ the specific external identity provided by federation.
For us, export the variable
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/sts-creds.json
Then when application default is used to call generateIdToken()
….well,
see the flow described here
id_token
to access cloud_runNow that we have a Google-issued id_token, use that to access Cloud Run
The full flow in python is like this"
#!/usr/bin/python
import jwt
from google.auth import credentials
from google.cloud import iam_credentials_v1
import google.auth
import google.oauth2.credentials
from google.auth.transport.requests import AuthorizedSession, Request
url = "https://federated-auth-cloud-run-6w42z6vi3q-uc.a.run.app/dump"
aud = "https://federated-auth-cloud-run-6w42z6vi3q-uc.a.run.app"
service_account = 'oidc-federated@your_project.iam.gserviceaccount.com'
## After this point ADC should run its course, exchange the fake oidc creds
## for a federated token (NOT oauth2 access_token since we skipped service_account_impersonation_url setting)
## the federated token is then used to invoke the IAM api which returns the final id_token we're interested in
client = iam_credentials_v1.services.iam_credentials.IAMCredentialsClient()
name = "projects/-/serviceAccounts/{}".format(service_account)
id_token = client.generate_id_token(name=name,audience=aud, include_email=True)
print(id_token.token)
r = jwt.decode(id_token, options={"verify_signature": False})
print("token expires at")
print(r["exp"])
creds = google.oauth2.credentials.Credentials(id_token.token)
authed_session = AuthorizedSession(creds)
r = authed_session.get(url)
print(r.status_code)
print(r.text)
This repo woudn’t be a repo without samples, see the clients/
folder for a language of your choice.
Note that for each, you must edit the source file and specify the url audience and service account you used.
Also remember to set export GOOGLE_APPLICATION_CREDENTIALS=/tmp/sts-creds.json
#!/usr/bin/python
from google.auth import credentials
from google.cloud import iam_credentials_v1
import google.auth
import google.oauth2.credentials
from google.auth.transport.requests import AuthorizedSession, Request
url = "https://federated-auth-cloud-run-6w42z6vi3q-uc.a.run.app/dump"
aud = "https://federated-auth-cloud-run-6w42z6vi3q-uc.a.run.app"
service_account = 'oidc-federated@your_project.iam.gserviceaccount.com'
client = iam_credentials_v1.services.iam_credentials.IAMCredentialsClient()
name = "projects/-/serviceAccounts/{}".format(service_account)
id_token = client.generate_id_token(name=name,audience=aud, include_email=True)
print(id_token.token)
creds = google.oauth2.credentials.Credentials(id_token.token)
authed_session = AuthorizedSession(creds)
r = authed_session.get(url)
print(r.status_code)
print(r.text)
package main
import (
"context"
"fmt"
"io/ioutil"
"log"
"net/http"
credentials "cloud.google.com/go/iam/credentials/apiv1"
credentialspb "google.golang.org/genproto/googleapis/iam/credentials/v1"
)
var ()
const (
url = "https://federated-auth-cloud-run-6w42z6vi3q-uc.a.run.app/dump"
aud = "https://federated-auth-cloud-run-6w42z6vi3q-uc.a.run.app"
serviceAccount = "oidc-federated@your_project.iam.gserviceaccount.com"
)
func main() {
ctx := context.Background()
c, err := credentials.NewIamCredentialsClient(ctx)
if err != nil {
log.Fatalf("%v", err)
}
defer c.Close()
idreq := &credentialspb.GenerateIdTokenRequest{
Name: fmt.Sprintf("projects/-/serviceAccounts/%s", serviceAccount),
Audience: aud,
IncludeEmail: true,
}
idresp, err := c.GenerateIdToken(ctx, idreq)
if err != nil {
log.Fatalf("%v", err)
}
log.Printf("IdToken %v", idresp.Token)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
log.Fatalf("%v", err)
}
req.Header.Add("Authorization", "Bearer "+idresp.Token)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Println("Error on response.\n[ERROR] -", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("Error while reading the response bytes:", err)
}
log.Println(string([]byte(body)))
}
package com.test;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import com.google.cloud.iam.credentials.v1.GenerateIdTokenRequest;
import com.google.cloud.iam.credentials.v1.GenerateIdTokenResponse;
import com.google.cloud.iam.credentials.v1.IamCredentialsClient;
import com.google.cloud.iam.credentials.v1.ServiceAccountName;
public class TestApp {
public static void main(String[] args) {
TestApp tc = new TestApp();
}
private static String url = "https://federated-auth-cloud-run-6w42z6vi3q-uc.a.run.app/dump";
private static String aud = "https://federated-auth-cloud-run-6w42z6vi3q-uc.a.run.app";
private static String serviceAccount = "oidc-federated@your_project.iam.gserviceaccount.com";
public TestApp() {
try {
// mvn -D"GOOGLE_APPLICATION_CREDENTIALS=/tmp/sts-creds.json" clean install exec:java -q
IamCredentialsClient iamCredentialsClient = IamCredentialsClient.create();
String name = ServiceAccountName.of("-", serviceAccount).toString();
GenerateIdTokenRequest idrequest = GenerateIdTokenRequest.newBuilder().setName(name).setAudience(aud)
.setIncludeEmail(true).build();
GenerateIdTokenResponse idresponse = iamCredentialsClient.generateIdToken(idrequest);
System.out.println("IDToken " + idresponse.getToken());
URL u = new URL(url);
HttpURLConnection conn = (HttpURLConnection) u.openConnection();
conn.setRequestProperty("Authorization", "Bearer " + idresponse.getToken());
conn.setRequestMethod("GET");
System.out.println("Response Code: " + conn.getResponseCode());
} catch (Exception ex) {
System.out.println("Error: " + ex.getMessage());
}
}
}
const { IAMCredentialsClient } = require('@google-cloud/iam-credentials');
const axios = require('axios');
async function main() {
const url = "https://federated-auth-cloud-run-6w42z6vi3q-uc.a.run.app/dump";
const aud = "https://federated-auth-cloud-run-6w42z6vi3q-uc.a.run.app";
const serviceAccount = "oidc-federated@your_project.iam.gserviceaccount.com";
const iam_client = new IAMCredentialsClient();
const [resp] = await iam_client.generateIdToken({
name: `projects/-/serviceAccounts/${serviceAccount}`,
audience: aud,
includeEmail: true
});
console.info(resp.token);
axios.get(url, {
headers: {
'Authorization': `Bearer ${resp.token}`
}
})
.then((res) => {
console.log(res.data)
})
.catch((error) => {
console.error(error)
})
}
main().catch(console.error);
not supported
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Google.Apis.Auth;
using Google.Apis.Http;
using Google.Apis.Auth.OAuth2;
using System.Net.Http;
using System.Net.Http.Headers;
using Google.Cloud.Iam.Credentials.V1;
// ERROR: Error reading credential file from location /tmp/sts-creds.json: Error creating credential from JSON. Unrecognized credential type external_account.
// Please check the value of the Environment Variable GOOGLE_APPLICATION_CREDENTIALS
namespace Program
{
public class Program
{
[STAThread]
static void Main(string[] args)
{
try
{
new Program().Run().Wait();
}
catch (AggregateException ex)
{
foreach (var err in ex.InnerExceptions)
{
Console.WriteLine("ERROR: " + err.Message);
}
}
}
public async Task<string> Run()
{
string url = "https://federated-auth-cloud-run-6w42z6vi3q-uc.a.run.app/dump";
string aud = "https://federated-auth-cloud-run-6w42z6vi3q-uc.a.run.app";
string serviceAccount = "oidc-federated@your_project.iam.gserviceaccount.com";
GoogleCredential sourceCredential = await GoogleCredential.GetApplicationDefaultAsync();
IAMCredentialsClient client = IAMCredentialsClient.Create();
GenerateIdTokenResponse resp = client.GenerateIdToken(new GenerateIdTokenRequest()
{
Name = "projects/-/serviceAccounts/" + serviceAccount,
Audience = aud,
IncludeEmail = true
});
Console.WriteLine("ID Token " + resp.Token);
using (var httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", resp.Token);
string response = await httpClient.GetStringAsync(url).ConfigureAwait(false);
Console.WriteLine(response);
return response;
}
}
}
}
This site supports webmentions. Send me a mention via this form.