This is a sample procedure that will exchange an arbitrary OIDC id_token
for a GCP credential.
You can use the GCP credential then to access any service the mapped principal has GCP IAM permissions on.
This article and repo is the second part that explores how to use the workload identity federation capability of GCP which allows for external principals (AWS,Azure or arbitrary OIDC provider) to map to a GCP credential.
The two variations described in this repo will acquire a Google Credential as described here:
The “Automatic” way is recommended and is supported by Google
The “Manual” way is also covered in this repo but I decided to wrap the steps for that into my own library here github.com/salrashid123/oauth2/google which surfaces the credential as an oauth2.TokenSource for use in any GCP cloud library.
NOTE: the library i’m using for the “manual” way is just there as an unsupported demo of a wrapped oauth2 TokenSource!
You can certainly use either procedure but the Automatic way is included with the supported, standard GCP Client library.
This repository is not supported by Google
salrashid123/oauth2/google
is also not supported by Google
also see
You can find the source here
GCP now surfaces a STS Service
that will exchange one set of tokens for another using the GCP Secure Token Service (STS) here. These initial tokens can be either 3rd party OIDC, AWS, Azure or google access_tokens
that are downscoped (i.,e attenuated in permission set).
To use this tutorial, you need a GCP project with Firebase as the OIDC Provider and the ability to create user/service accounts.
Again, the two types of flows this repo demonstrates:
Manual Exchange:
In this you manually do all the steps of exchanging a Firebase/Identity Platform id_token
for a federated token and then finally use that token
Automatic Exchange In this you use the google cloud client libraries to do all the heavy lifting. « This is the recommended approach
It is recommended to do the manual first just to understand this capability and then move onto the automatic
This tutorial will cover how to use an OIDC token and its claims to a GCP principal:// and principalSet://
User: principal://
This maps a unique user identified by OIDC to a GCP identity
Group: principalSet://
This maps any user that has a given attrubute or group declared in the OIDC token to a GCP identity
In both cases, the GCP Identity is a Service Account that the external user impersonates.
First we need an OIDC token provider that will give us an id_token
. Just for demonstration, we will use Google Cloud Identity Platform as the provider (you can of course use okta, auth0, even google itself).
The GCP project i am using in the example here is called fabled-ray-104117
. Identity platform will automatically create a ‘bare bones’ oidc .well-known
endpoint at a url that includes the projectID:
The following shows how to acquire an OIDC token for use with this tutorial. As mentioned, we are using FirebaseAuth/Identity Platform; you can use any other provider as long as the .well-known
endpoint is discoverable by GCP
Enable Identity Platform
Add Email/Password
as the provider
Note the API_KEY
and authDomain value
Edit login.js
and enter in the API Key/AuthDomain
In my case, it is:
var firebaseConfig = {
apiKey: "AIzaSyAf7wesN7auBeyfJQJ--redacted",
authDomain: "fabled-ray-104117.firebaseapp.com",
projectId: "fabled-ray-104117",
appId: "fabled-ray-104117",
};
Generate a service account and download it from the firebase console: (note, replace with your projectID in the URL field below)
Save the file as /tmp/svc_account.json
npm i firebase firebase-admin
$ export GOOGLE_APPLICATION_CREDENTIALS=/tmp/svc_account.json
$ node create.js
{ uid: 'alice@domain.com',
email: 'alice@domain.com',
emailVerified: true,
displayName: 'alice',
photoURL: undefined,
phoneNumber: undefined,
disabled: false,
metadata:
{ lastSignInTime: null,
creationTime: 'Mon, 26 Jul 2021 18:10:50 GMT' },
passwordHash: undefined,
passwordSalt: undefined,
customClaims: { isadmin: 'true', mygroups: [ 'group1', 'group2' ] },
tokensValidAfterTime: 'Mon, 26 Jul 2021 18:10:50 GMT',
tenantId: undefined,
providerData:
[ { uid: 'alice@domain.com',
displayName: 'alice',
email: 'alice@domain.com',
photoURL: undefined,
providerId: 'password',
phoneNumber: undefined } ] }
At this pont, user Alice has a custom claim associated with the user. Empirically, the attribute values must be string (i.e, i intentionally set isadmin
to (string) true
(not boolean))
The following script actually performs a login and displays the JSON response a firebase/identity platform user would see (i.,e they would see that struct after logging in the browser too)
$ node login.js
{
"user": {
"uid": "alice@domain.com",
"displayName": "alice",
"photoURL": null,
"email": "alice@domain.com",
"emailVerified": true,
"phoneNumber": null,
"isAnonymous": false,
"tenantId": null,
"providerData": [
{
"uid": "alice@domain.com",
"displayName": "alice",
"photoURL": null,
"email": "alice@domain.com",
"phoneNumber": null,
"providerId": "password"
}
],
"apiKey": "AIzaSyAf7wesN7auBeyfJQJs5d_QfT24kMH7OG8",
"appName": "[DEFAULT]",
"authDomain": "fabled-ray-104117.firebaseapp.com",
"stsTokenManager": {
"apiKey": "AIzaSyAf7wesN7auBeyfJQJs5d_QfT24kMH7OG8",
"refreshToken": "AG8BCncodfNZo5RjfUIaayD-redacted",
"accessToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFiYjk2MDVjMzZlOThlMzAxMTdhNjk1MTc1NjkzODY4MzAyMDJiMmQiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiYWxpY2UiLCJpc2FkbWluIjoidHJ1ZSIsIm15Z3JvdXBzIjpbImdyb3VwMSIsImdyb3VwMiJdLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vZmFibGVkLXJheS0xMDQxMTciLCJhdWQiOiJmYWJsZWQtcmF5LTEwNDExNyIsImF1dGhfdGltZSI6MTYyNzMyMzEwOSwidXNlcl9pZCI6ImFsaWNlQGRvbWFpbi5jb20iLCJzdWIiOiJhbGljZUBkb21haW4uY29tIiwiaWF0IjoxNjI3MzIzMTA5LCJleHAiOjE2MjczMjY3MDksImVtYWlsIjoiYWxpY2VAZG9tYWluLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbImFsaWNlQGRvbWFpbi5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9fQ.lEJZ-tHRCOAR0kKiN9AoAB6Y7HXoSeQDQnDW2QqA1ouJd_nudlcjMW4tjsdpowtAmHprytPRsrJ35zz8r5BXu_jDX5k-aASIUw4Eg9mk1eCxXGIZn6Pv54K7MBMq-2lgzaTkpvNjX9-3jlUagOd6U0cbeTHFk4_9yA4jAS0dp2sixx_dQer0qRXJ96EIZ4XsGh_6to4DdG4n6k0V25vNchKd0vuM5G14yb2lpxmu9yE0EEXrujzgrVErFOzLZd4xeBR6svn6X-9NmYf3ffoyQv_ZF0bCTligQsBApg2aa95guig8jA5Gsf-XnuPWmSTcpu1PG54Zzw78enylr3WyHw",
"expirationTime": 1603627901000
},
"redirectEventId": null,
"lastLoginAt": "1603624301973",
"createdAt": "1603624274304",
"multiFactor": {
"enrolledFactors": []
}
},
"credential": null,
"additionalUserInfo": {
"providerId": "password",
"isNewUser": false
},
"operationType": "signIn"
}
export OIDC_TOKEN=`node login.js | jq -r '.user.stsTokenManager.accessToken'`
echo $OIDC_TOKEN > /tmp/oidccred.txt
The access_token
is actually a JWT id_token which you can decode at jwt.io:
Notice the isadmin
and sub
fields there
{
"name": "alice",
"isadmin": "true",
"mygroups": [
"group1",
"group2"
],
"iss": "https://securetoken.google.com/fabled-ray-104117",
"aud": "fabled-ray-104117",
"auth_time": 1627323109,
"user_id": "alice@domain.com",
"sub": "alice@domain.com",
"iat": 1627323109,
"exp": 1627326709,
"email": "alice@domain.com",
"email_verified": true,
"firebase": {
"identities": {
"email": [
"alice@domain.com"
]
},
"sign_in_provider": "password"
}
}
Some things to note
issuer
is https://securetoken.google.com/mineral-minutia-820
,sub
field describes the usernameisadmin
is a custom claim which we will use for the principalSet://
attribute mappingmygroups.*
is a custom claim which we will use for the principalSet://
group mappingWe can now configure the GCP project for OIDC Federation
export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format='value(projectNumber)'`
gcloud beta iam workload-identity-pools create oidc-pool-1 \
--location="global" \
--description="OIDC Pool " \
--display-name="OIDC Pool" --project $PROJECT_ID
Configure provider
The following command will configure the provider itself. Notice that we specify the issuer URL without the .well-known
URL path (since its, well, well-known)
gcloud beta iam workload-identity-pools providers create-oidc oidc-provider-1 \
--workload-identity-pool="oidc-pool-1" \
--issuer-uri="https://securetoken.google.com/fabled-ray-104117/" \
--location="global" \
--attribute-mapping="google.subject=assertion.sub,attribute.isadmin=assertion.isadmin,attribute.aud=assertion.aud" \
--attribute-condition="attribute.isadmin=='true' && attribute.aud=='fabled-ray-104117'" --project $PROJECT_ID
Notice the attribute mapping:
google.subject=assertion.sub
: This will extract and populate the google subject value from the provided id_token’s sub
field.attribute.isadmin=assertion.isadmin
: This will extract the value of the custom claim isadmin
and then make it available for IAM rule later as an assertionNotice the attribute conditions:
attribute.isadmin=='true'
: This describes the condition that this provider must meet. The provided idToken’s isadmin
field MUST be set to trueattribute.aud=='fabled-ray-104117'
: This describes the audience value in the token must be set to the project you are using (in my case fabled-ray-104117
)If you set the attribute conditions to something else, you should see an error during authentication:
Unable to exchange token {"error":"unauthorized_client","error_description":"The given credential is rejected by the attribute condition."},
For group mapping
gcloud beta iam workload-identity-pools providers create-oidc oidc-provider-2 \
--workload-identity-pool="oidc-pool-1" \
--issuer-uri="https://securetoken.google.com/fabled-ray-104117/" \
--location="global" \
--attribute-mapping="google.subject=assertion.sub,google.groups=assertion.mygroups,attribute.aud=assertion.aud" \
--attribute-condition="attribute.aud=='fabled-ray-104117'" --project $PROJECT_ID
Create GCS Resource
Create a test GCP resource like GCS and upload a file
gsutil mb gs://$PROJECT_ID-test
echo fooooo > foo.txt
gsutil cp foo.txt gs://$PROJECT_ID-test
Crate Service Account
By default, the OIDC identity will need to map to a Service Account which inturn will have access to the GCS resource. The external identity will get validated by GCP and then will impersonate a GCP Service Account
gcloud iam service-accounts create oidc-federated
This allows a single user alice@domain.com
permissions 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-1/subject/alice@domain.com"
This allows any user part of the OIDC issuer that has the claim embedded in the token with key-value isadmin==true
gcloud iam service-accounts add-iam-policy-binding oidc-federated@$PROJECT_ID.iam.gserviceaccount.com \
--role roles/iam.workloadIdentityUser \
--member "principalSet://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-pool-1/attribute.isadmin/true"
The following allows any user that is in group
of the claim permission to impersonate this service account
gcloud iam service-accounts add-iam-policy-binding oidc-federated@$PROJECT_ID.iam.gserviceaccount.com \
--role roles/iam.workloadIdentityUser \
--member "principalSet://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-pool-1/group/group1"
gsutil iam ch serviceAccount:oidc-federated@$PROJECT_ID.iam.gserviceaccount.com:objectViewer gs://$PROJECT_ID-test
Before you run the sample, you must first get an OIDC Token. See Create OIDC token using Identity Platform
above
export OIDC_TOKEN=`node login.js | jq -r '.user.stsTokenManager.accessToken'`
echo $OIDC_TOKEN > /tmp/oidccred.txt
At this point, we are ready to use the OIDC token and exchange it manually
$ go run main.go \
--gcpBucket $PROJECT_ID-test \
--gcpObjectName foo.txt \
--gcpResource //iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-pool-1/providers/oidc-provider-1 \
--gcpTargetServiceAccount oidc-federated@$PROJECT_ID.iam.gserviceaccount.com \
--useIAMToken \
--sourceToken $OIDC_TOKEN
2020/10/24 07:16:14 OIDC Derived GCP access_token: ya29.c.KuQC4gf-xkKbOCIzRGAmAPdL2unF4vLCjZG7TZv7l7bjCK67n2qduIFDs63HR...
fooooo
$ go run main.go \
--gcpBucket $PROJECT_ID-test \
--gcpObjectName foo.txt \
--gcpResource //iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-pool-1/providers/oidc-provider-2 \
--gcpTargetServiceAccount oidc-federated@$PROJECT_ID.iam.gserviceaccount.com \
--useIAMToken \
--sourceToken $OIDC_TOKEN
2020/10/24 07:16:14 OIDC Derived GCP access_token: ya29.c.KuQC4gf-xkKbOCIzRGAmAPdL2unF4vLCjZG7TZv7l7bjCK67n2qduIFDs63HR...
fooooo
What you should see is the output of the GCS file
We are now ready to use the Automatic Application Default Credentials to access the ressource
First configure the ADC bootstrap file:
gcloud beta iam workload-identity-pools create-cred-config \
projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-pool-1/providers/oidc-provider-1 \
--service-account=oidc-federated@$PROJECT_ID.iam.gserviceaccount.com \
--output-file=sts-creds.json \
--credential-source-file=/tmp/oidccred.txt
The output/bootstrap file should look something like this:
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/1071284184436/locations/global/workloadIdentityPools/oidc-pool-1/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"
},
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/oidc-federated@cicp-oidc-test.iam.gserviceaccount.com:generateAccessToken"
}
Notice the bootstrap file has a pointer to the file where the actual creds exist /tmp/oidccreds.txt
. (eventually other cred sources should be supported)
Before you run the sample, you must first get an OIDC Token. See Create OIDC token using Identity Platform
below
Remember
/tmp/oidccred.txt
has the content of the raw OIDC token to use (i.,e is the value of $OIDC_TOKEN)
export OIDC_TOKEN=`node login.js | jq -r '.user.stsTokenManager.accessToken'`
echo $OIDC_TOKEN > /tmp/oidccred.txt
export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/sts-creds.json
go run main.go --gcpBucket $PROJECT_ID-test --gcpObjectName foo.txt --useADC
If you would rather use
google.group=
mapping wiht aprincipalSet://
, change thecreate-cred-config
to useoidc-provider-2
At the time of writing (3/14/21), the configuration file (only supports reading of a file that contains the oidc token (/tmp/oidccred.txt
) directly. Eventually, other mechanisms like url or executing a binary that returns the oidc token will be supported
GCP STS Tokens can be used directly against a few GCP services as described here
Skip step (5)
of Exchange Token
What that means is you can skip the step to exchange the GCP Federation token for an Service Account token and directly apply IAM policies on the resource.
This not only saves the step of running the exchange but omits the need for a secondary GCP service account to impersonate.
To use GCS, allow either the mapped identity direct access to the resource. In this case storage.objectAdmin
access which we already allowed earlier:
Allow Federated Identity IAM access
Configure the federated identity access to GCS bucket.
This allows a single user alice@domain.com
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member "principal://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-pool-1/subject/alice@domain.com" \
--role roles/storage.objectAdmin
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member "principalSet://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-pool-1/attribute.isadmin/true" \
--role roles/storage.objectAdmin
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member "principalSet://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/oidc-pool-1/group/group1" \
--role roles/storage.objectAdmin
Notice that in this mode we are directly allowing the federated identity access to a GCS resource
To use Federated tokens, use remove the --useIAMToken
flag
If you want to use Federated tokens only with the Automatic flow, delete
service_account_impersonation_url
declaration insts-creds.json
If you used the STS token directly, the principal will appear in the GCS logs if you enabled audit logging
If you used IAM impersonation, you will see the principal performing the impersonation
and then the impersonated account accessing GCS
Notice the protoPayload.authenticationInfo
structure between the two types of auth
You can also define a GCP Organization Policy that restricts which providers can be enabled for federation
constraints/iam.workloadIdentityPoolProviders
For example, for the following test organization, we will define a policy that only allows you to create a workload identity using a the specified OIDC providers URL
$ gcloud organizations list
DISPLAY_NAME ID DIRECTORY_CUSTOMER_ID
esodemoapp2.com 673208786092 redacted
$ gcloud resource-manager org-policies allow constraints/iam.workloadIdentityPoolProviders \
--organization=673208786092 https://securetoken.google.com/cicp-oidc-test/
constraint: constraints/iam.workloadIdentityPoolProviders
etag: BwWybJWeyeU=
listPolicy:
allowedValues:
- https://securetoken.google.com/cicp-oidc-test/
updateTime: '2020-10-24T15:45:19.794Z'
$ gcloud beta iam workload-identity-pools providers create-oidc oidc-provider-3 \
--workload-identity-pool="oidc-pool-1" \
--issuer-uri="https://securetoken.google.com/foo/" \
--location="global" \
--attribute-mapping="google.subject=assertion.sub,attribute.isadmin=assertion.isadmin,attribute.aud=assertion.aud" \
--attribute-condition="attribute.isadmin=='true' && attribute.aud=='cicp-oidc-test'"
ERROR: (gcloud.beta.iam.workload-identity-pools.providers.create-oidc) FAILED_PRECONDITION: Precondition check failed.
- '@type': type.googleapis.com/google.rpc.PreconditionFailure
violations:
- description: "Org Policy violated for value: 'https://securetoken.google.com/foo/'."
subject: orgpolicy:projects/user2project2/locations/global/workloadIdentityPools/oidc-pool-1
type: constraints/iam.workloadIdentityPoolProviders
This site supports webmentions. Send me a mention via this form.