Using Workforce Identity Federation and Entra App roles to control access to Google Cloud
Google Cloud IAM implements discretionary access control, meaning each resource - be it a folder, project, or VM - has an IAM policy. We can think of this policy as an access control list, outlining who can access the resource and what actions they’re permitted to take.
This model is extremely flexible. But as we scale, management can become a challenge: As the number of resources, IAM policies, and users grow, controlling access on a per-resource, per-user basis quickly becomes unviable.
To make discretionary access control practical at scale, we have to use groups. In Cloud Identity and Workspace, groups are first-class citizens: We can create them in the Admin Console, via API, have Entra provision them, or leverage tools like JIT groups.
However, Cloud Identity and Workspace groups only work with Google identities, they don’t support workforce identity federation. This means we can’t add a workforce identity principal to a Cloud Identity group, and this is by design.
To effectively use groups with workforce identity federation, we need Entra ID to pass the user’s group memberships in the OIDC token or SAML assertion. Then, within IAM, we can use a “groups principal set” to consume these groups and grant them access to resouces
For example, if a user’s SAML assertion contains a groups
attribute such as:
<Assertion ...>
<Issuer>https://sts.windows.net/b9f4725c-...</Issuer>
...
<Subject>
<NameID Format="...">[email protected]</NameID>
...
</Subject>
<AttributeStatement>
<Attribute Name="http://schemas.microsoft.com/ws/2008/06/identity/claims/groups">
<AttributeValue>database-admins</AttributeValue>
</Attribute>
...
</AttributeStatement>
</samlp:Response>
Then the user “matches” the principal identifier
principalSet://iam.googleapis.com/locations/global/ workforcePools/POOL_ID/group/database-admins
,
and can access any resource that this principal identifier has been granted access to.
Mapping group claims
The most intuitive way to populate the groups
claim (or attribute, in case of SAML) is to let
it contain the list of groups that the user is a member of. However, since group claims are simply
strings, we must decide which identifier to use.
One option is the group’s object ID (which is a GUID like
9cffe4c2-721e-44c4-a037-541e2f5d13dd
). This identifier is globally unique and unlikely to be reused, making the object ID a great choice from a security perspective. But from a usability perspective, object IDs are a nightmare.Trying to decipher a principal identifier like
principalSet://iam.googleapis.com/locations/global/ workforcePools/POOL_ID/group/9cffe4c2-721e-44c4-a037-541e2f5d13dd
in logs, IAM policies, or the Cloud Console UI is nearly impossible. We’re left clueless about which user group it represents, making IAM cumbersome to use.Another option is to map the group’s display name (for example,
database-admins
). This solves the usability issue, but introduces a new set of problems and risks. For instance, if we rename a group that’s already been used to grant access to Google Cloud resources, users will lose access because the group name in the assertion no longer matches the one used in IAM policies.Having IAM policies reference groups that no longer exist (because they’ve been renamed or deleted) also creates a security risk: A bad actor could create a group in Entra ID, intentionally name it so that it matches the existing IAM policy, and then add themselves to the group (reminiscent of the VMWare ESXi Admins CVE).
Both options are bad in different ways. So what’s the alternative?
Mapping App roles to claims
Instead of populating the groups
claim with the user’s groups, we can populate it with the user’s
App roles.
App roles are specifically designed for access management, and they use identifiers that nicely match what workforce identity federation needs: AppRole identifiers are strings, human-readable, and unique (at least within the context of one app registration). This makes App roles the superior, albeit less obvious choice.
Setting up Entra to use App roles
Let’s set up workforce identity federation with App roles.
Create a workforce pool
First, we need to create a workforce pool in Google Cloud:
- Open the Cloud Console and open Cloud Shell
Create a workforce pool:
gcloud iam workforce-pools create POOL_ID \ --organization ORGANIZATION_ID \ --display-name "Entra ID" \ --location global
Replace the following:
POOL_ID
: an ID that you choose to represent your Google Cloud workforce pool.ORGANIZATION_ID
: the numeric organization ID of your Google Cloud organization.
Note that we need the IAM Workforce Pool Admin role to run this command.
Create an application in Entra ID
We now have a workforce pool and have claimed a pool ID (which is a global identifier). That’s everything all we need to create the application in Entra:
- In the Azure portal, open Cloud Shell and select PowerShell.
Register Google Cloud as an Azure AD application by running the following command in Azure Cloud Shell.
$Pool='POOL_ID' # From previous step $Provider='oidc' $App = New-AzADApplication ` -DisplayName 'Google Cloud (Workforce Identity)' ` -ReplyUrl 'https://auth.cloud.google/signin-callback/locations/global/workforcePools/$Pool/providers/$Provider' ` -SignInAudience AzureADMyOrg ` -AvailableToOtherTenants $False ` -Homepage "https://auth.cloud.google/signin/locations/global/workforcePools/$Pool/providers/$Provider?continueUrl=https%3A%2F%2Fconsole.cloud.google%2F" ` -RequiredResourceAccess @{ ResourceAppId = "00000003-0000-0000-c000-000000000000"; # Graph API ResourceAccess = @( @{ Id = "14dad69e-099b-42c9-810b-d002981feec1"; # profile Type = "Scope" }, @{ Id = "37f7f235-527c-4136-accd-4a02d197296e"; # openid Type = "Scope" } ) } ` -GroupMembershipClaim ApplicationGroup ` -OptionalClaim @{ IdToken = @( @{ Name = "upn" } ) }
Replace
POOL_ID
with the ID of our workforce pool.The command configures the application so that:
- Google Cloud can obtain a user’s UPN and basic profile information during a sign-in.
- Users from other tenants can’t use the application.
Create a client secret for the application:
$Credential = $App | New-AzADAppCredential
Add Google Cloud to your list of enterprise applications by creating a service principal:
$ServicePrincipal = New-AzADServicePrincipal ` -ApplicationId $App.AppId ` -AppRoleAssignmentRequired ` -Tag 'WindowsAzureActiveDirectoryIntegratedApp'
The command configures the service principal so that:
- You can view and configure access for Google Cloud in the Azure Portal under Enterprise Applications.
- Only assigned users and groups are allowed to access Google Cloud.
- Assigned users can find Google Cloud in their My Apps portal.
Retrieve the tenant ID and client ID:
@{ "Tenant ID" = $env:ACC_TID "Client ID" = $App.AppId "Client Secret" = $Credential.SecretText "Client Secret Expiry" = $Credential.EndDateTime }
We need these IDs in the following step.
Create a workforce pool provider
We now switch back to Google Cloud and add a provider to our workforce pool:
Return to Cloud Shell and initialize the following variables:
POOL=WORKFORCE_POOL_ID TENANT_ID=ENTRA_TENANT_ID CLIENT_ID=ENTRA_CLIENT_ID CLIENT_SECRET=ENTRA_CLIENT_SECRET
Replace the following:
WORKFORCE_POOL_ID
: ID of our workforce pool.ENTRA_TENANT_ID
: the tenant ID of our Entra tenant, we can copy that from the Azure Cloud Shell output.ENTRA_CLIENT_ID
andENTRA_CLIENT_SECRET
: the client ID and secret from the Azure Cloud Shell output.
Add a provider to our workforce pool:
gcloud iam workforce-pools providers create-oidc oidc \ --location global \ --workforce-pool $POOL \ --attribute-mapping "google.subject=assertion.upn, google.groups = assertion.roles, google.display_name = assertion.name" \ --issuer-uri "https://login.microsoftonline.com/$TENANT_ID/v2.0/" \ --web-sso-response-type code \ --web-sso-assertion-claims-behavior only-id-token-claims \ --client-id $CLIENT_ID \ --client-secret-value $CLIENT_SECRET \ --display-name "Entra ID (OIDC)"
Create an App role
Our workforce identity federation setup is almost complete, but we still need to create some App roles to test with:
- Return to the Azure portal.
- Go to App registrations > Google Cloud (Workforce Identity) > App roles.
Add an App role, for example:
- Display name:
Dev.MyWorkoad.DatabaseAdmins
- Value:
Dev.MyWorkoad.DatabaseAdmins
- Description: Grants administrative access to the databases used by “MyWorkload” in the DEV environment
The Value will be what’s being passed to Google Cloud in the ID token.
- Display name:
Add additional App roles as needed.
Finally, we need to grant admin consent for the application:
- Go to App registrations > Google Cloud (Workforce Identity) > API permissions.
- Click Grant admin consent.
Now let’s test the setup. For that, we need a user:
- In the Azure portal, go to Enterprise applications > Google Cloud (Workforce Identity) > Users and groups.
Add a user, and assign them one or more App roles:
Open an incognito browser window and open the following URL:
https://auth.cloud.google/signin/locations/global/workforcePools/POOL/providers/oidc?continueUrl=https://console.cloud.google/
Replace
POOL
with the ID of our workforce pool.Sign in as the user that we’ve previously added to the application.
Behind the scenes, Google Cloud fetches an ID token for the user, and uses that to initiate a Cloud Console session. This ID token looks like this:
{
"typ": "JWT",
"alg": "RS256",
"kid": "JDNa_4i4r7FgigL3sHIlI3xV-IU"
}.{
"aud": "87305bb6…",
"iss": "https://login.microsoftonline.com/b9f4725c…",
"iat": 1742425992,
"nbf": 1742425992,
"exp": 1742429892,
"name": "Alain",
"oid": "6ca3978b-2b1c-48a3-a8e8-bbdf9f3f8d17",
"preferred_username": "[email protected]",
"rh": "1.AWY...",
"roles": [
"Dev.MyWorkoad.DatabaseAdmins"
],
"sid": "0030b4d9…",
"sub": "4Obw4x…",
"tid": "b9f4725c…",
"upn": "[email protected]",
"uti": "fx-J…",
"ver": "2.0"
}.[Signature]
Notice the upn
and roles
claims.
Google Cloud, or more precisely the STS, verifies this token and applies the attribute mapping that we specified when creating the provider:
google.subject = assertion.upn
google.groups = assertion.roles
google.display_name = assertion.name
As a result, the user is assigned the following principal identifiers:
principal://iam.googleapis.com/locations/global/ workforcePools/POOL_ID/subject/[email protected]
(based on the thegoogle.subject
attribute mapping).principalSet://iam.googleapis.com/locations/global/ workforcePools/POOL_ID/group/Dev.MyWorkoad.DatabaseAdmins
(based on thegoogle.groups
attribute mapping).
If the token contained an additional app role, say Dev.MyWorkoad.DatabaseReaders
, then the user would also match
the following principal identifier:
principalSet://iam.googleapis.com/locations/global/ workforcePools/POOL_ID/group/Dev.MyWorkoad.DatabaseReaders
These principal identifiers are still uncomfortably long – but they’re human-readable, meaningful, and stable.