Impersonation and Delegation is often used interchangeably but on Google CLoud, these are two distinct mechanism. GCP uses Impersonation to mean service account impersonation while Delegation means acting as a workspace through Domain-wide delegation.
user|service_account_A
acts as service_account_B
–> access GCP Resourceservice_account_A
acts as user
–> access GCP or Workspace resourceThis article focuses primarily on Delegation and how to construct cloud sdk clients to authenticate as users. For impersonation, see Using ImpersonatedCredentials for Google Cloud APIs and IDTokens and the corresponding git repoo Using ImpersonatedCredentials for Google Cloud APIs and IDTokens
The following set describes how to enable domain delegation in various cloud client libraries.
Domain wide Delegation (dwd) is performed when a Service Account that is enabled for dwd
issues a specially constructed JWT using its private key. This JWT has a subject (sub:
) claim that specifies which users token to return. Once this JWT is sent to Google, the signature is checked along with the scopes (another field in the jwt), then finally the user’s access_token
is returned.
See the section in the link below titled additional claims
"In some enterprise cases, an application can use domain-wide delegation to act on behalf of a particular user in an organization. Permission to perform this type of impersonation must be granted before an application can impersonate a user, and is usually handled by a super administrator. For more information, see Control API access with domain-wide delegation.
To obtain an access token that grants an application delegated access to a resource, include the email address of the user in the JWT claim set as the value of the sub field.
Name Description
sub The email address of the user for which the application is requesting delegated access.
If an application does not have permission to impersonate a user, the response to an access token request that includes the sub field will be an error.
An example of a JWT claim set that includes the sub field is shown below:
{
"iss": "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com",
"sub": "user@domain.com",
"scope": "https://www.googleapis.com/auth/prediction",
"aud": "https://oauth2.googleapis.com/token",
"exp": 1328554385,
"iat": 1328550785
}
Critically, the service account requires some way sign()
the JWT in the first place which means the application needs direct or indirect access to the private key for that service account.
Direct access is easy: download the service_account key and use the default flow shown in the samples below
Indirect requires the identity that running the application IAM permissions to sign()
bytes on behalf of the service account without the key being present
The challenge is how a service account can sign without a local key. On GCP, this is achieved by allowing the identity to use an API to sign with a key that resides only within Google. That is use iamcredentials.signBlob() API and pass the bytes for the constructed JWT.
In other words, if an source identity has the iam.serviceAccounts.signBlob
permission (granted by Role roles/iam.serviceAccountTokenCreator
) on the service account authorized to perform domain delegation, the source identity can generate a JWT with the appropriate sub:
field, ask IAM API to sign the JWT and finally exchange that signedJWT for the subject’s access_token
To be clear, we are getting a users token (delegated) though impersonation:
Assume service_2
has domain delegation rights and user_1
or service_1
has permissions to impersonate service_2
, the flow becomes:
user_1|service_1
-> impersonate(service_2)
-> delegate(user_2)
-> gcp|workspace resource
One common scenario is if service_1
is a static service account on GCE|GKE (which does NOT have a signer), in that scenario, service_1
will need “self-impersonation” permissions (i.,e the tokenCreator role granted on itself)
In terms of library support several already support this flow like though
In other languages, the flow must be done manually:
In python, a signer()
is surfaced though google.auth.credentials.Signing
interface in both
Signing
interface was “just there”)).
Both flows are shown in this article for python only.In Java, I’ve written a wrapper library here Easy GSuites Domain-Wide Delegation (DwD) in Java that will accept various source credentials capable of signing
The iam.serviceAccountTokenCreator
role includes concentric set of permission (see Concentric IAMCredentials Permissions: The secret life of signBlob. If you just need to generate an access_token
or an id_token
, consider generating a custom role with that specific capability.
Sample Domain-Wide Delegation Setup:
Create Service Account Enable domain-wide delegation and note the clientID:
Assign GSuite scopes to client_id On the gsuite admin console under “Security > Advanced Settings > Manage API client access”, enter the client_id and the scopes this service account should be allowed to use when requesting a users credentials:
Specify the scopes intended to be used as using comma seprated values.
We are using read-only on the user just to test
https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/admin.directory.user.readonly
Once the steps above are done, the service account itself can assume the identity of any user’s account in the domain and act as that user. Note that we are using a service_account key for these samples for convenience Using a key has security implications. Please be careful with the key (or remove it entirely)
project='your-project-id'
import google.auth
from google.oauth2 import service_account
target_scopes = ["https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/admin.directory.user.readonly"]
credentials = service_account.Credentials.from_service_account_file(
'/path/to/svc_account.json',
scopes=target_scopes,
subject='user@domain.com')
from google.cloud import storage
client = storage.Client(project=project, credentials=credentials)
for b in client.list_buckets():
print(b.name)
from google.cloud import pubsub_v1
from google.auth.transport.requests import AuthorizedSession
project_path = f"projects/{project}"
authed_session = AuthorizedSession(credentials)
response = authed_session.request('GET', 'https://pubsub.googleapis.com/v1/{}/topics'.format(project_path))
print(response.json())
## bug: https://github.com/googleapis/google-auth-library-python/issues/929
publisher = pubsub_v1.PublisherClient(credentials=credentials)
for topic in publisher.list_topics(request={"project": project_path}):
print(topic.name)
Please note that the snippet above uses a service_account key file which is not recommended. The
In go, domain delegation is enabled if just one parameter is added into the constructor here
package main
import (
"fmt"
"io/ioutil"
pubsub "cloud.google.com/go/pubsub"
storage "cloud.google.com/go/storage"
"golang.org/x/net/context"
"golang.org/x/oauth2/google"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
)
const (
projectID = "your-project-id"
)
func main() {
ctx := context.Background()
data, err := ioutil.ReadFile("/path/to/svc_account.json")
if err != nil {
fmt.Println(err)
return
}
credentials, err := google.CredentialsFromJSONWithParams(ctx, data, google.CredentialsParams{
Scopes: []string{"https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/admin.directory.user.readonly"},
Subject: "user@domain.com",
})
if err != nil {
fmt.Printf("error getting credentials: %v", err)
return
}
storageClient, err := storage.NewClient(ctx, option.WithCredentials(credentials))
if err != nil {
fmt.Printf("storage.NewClient: %v", err)
return
}
defer storageClient.Close()
it := storageClient.Buckets(ctx, projectID)
for {
battrs, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
fmt.Printf("storage.Iterating error: %v", err)
return
}
fmt.Printf("Bucket Name: %s\n", battrs.Name)
}
// *******************************************
pubsubClient, err := pubsub.NewClient(ctx, projectID, option.WithCredentials(credentials))
if err != nil {
fmt.Printf("pubsub.NewClient: %v", err)
return
}
defer pubsubClient.Close()
pit := pubsubClient.Topics(ctx)
for {
topic, err := pit.Next()
if err == iterator.Done {
break
}
if err != nil {
fmt.Printf("pubssub.Iterating error: %v", err)
return
}
fmt.Printf("Topic Name: %s\n", topic.ID())
}
}
Alternatively, if running in an environment where impersonated credentials could be use,
ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{
TargetPrincipal: "foo@project-id.iam.gserviceaccount.com",
Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"},
Subject: "admin@example.com",
})
In the snippet above, foo@project-id.iam.gserviceaccount.com
is a service account that will be impersonated but that service account has
permissions to do domain delegation to "admin@example.com"
. The resulting tokensource can be used to make GCP api calls as if its by “admin@example.com”
package com.test;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;
import com.google.api.gax.core.FixedCredentialsProvider;
import com.google.auth.oauth2.DomainDelegatedCredentials;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.pubsub.v1.TopicAdminClient;
import com.google.cloud.pubsub.v1.TopicAdminClient.ListTopicsPagedResponse;
import com.google.cloud.pubsub.v1.TopicAdminSettings;
import com.google.cloud.storage.Bucket;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import com.google.pubsub.v1.ListTopicsRequest;
import com.google.pubsub.v1.ProjectName;
import com.google.pubsub.v1.Topic;
public class TestApp {
public static void main(String[] args) {
TestApp tc = new TestApp();
}
public TestApp() {
try {
String sub = "user@domain.com";
List<String> scopes = new ArrayList<String>();
scopes.add("https://www.googleapis.com/auth/admin.directory.user.readonly");
scopes.add("https://www.googleapis.com/auth/cloud-platform");
String keyFile = "/path/to/svc_account.json";
GoogleCredentials sourceCredentials;
File credentialsPath = new File(keyFile);
try (FileInputStream serviceAccountStream = new FileInputStream(credentialsPath)) {
sourceCredentials = ServiceAccountCredentials.fromStream(serviceAccountStream);
}
GoogleCredentials credentials = DomainDelegatedCredentials.create(sourceCredentials, sub, scopes);
Storage storage_service = StorageOptions.newBuilder().setProjectId("your-project-id")
.setCredentials(credentials).build().getService();
for (Bucket b : storage_service.list().iterateAll()) {
System.out.println(b);
}
TopicAdminClient topicClient = TopicAdminClient.create(TopicAdminSettings.newBuilder()
.setCredentialsProvider(FixedCredentialsProvider.create(credentials)).build());
ListTopicsRequest listTopicsRequest = ListTopicsRequest.newBuilder()
.setProject(ProjectName.format("your-project-id"))
.build();
ListTopicsPagedResponse response = topicClient.listTopics(listTopicsRequest);
Iterable<Topic> topics = response.iterateAll();
for (Topic topic : topics)
System.out.println(topic);
} catch (Exception ex) {
System.out.println("Error: " + ex);
}
}
}
var log4js = require("log4js");
var logger = log4js.getLogger();
const { PubSub } = require('@google-cloud/pubsub');
const { Storage } = require('@google-cloud/storage');
const { JWT, OAuth2Client } = require('google-auth-library');
let projectId = 'your-project-id'
const key = require('/path/to/svc_account.json');
const jwtClient = new JWT(
key.client_email,
null,
key.private_key,
["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/admin.directory.user.readonly"],
"user@domain.com"
);
const oauth2Client = new OAuth2Client();
oauth2Client.refreshHandler = async () => {
const refreshedAccessToken = await jwtClient.getAccessToken();
return {
access_token: refreshedAccessToken.token,
expiry_date: refreshedAccessToken.expirationTime,
};
};
const authOptions = {
projectId,
authClient: {
getCredentials: async () => {
return {
client_email: key.client_email
}
},
request: opts => {
return oauth2Client.request(opts);
},
authorizeRequest: async opts => {
opts = opts || {};
const url = opts.url || opts.uri;
const headers = await oauth2Client.getRequestHeaders(url);
opts.headers = Object.assign(opts.headers || {}, headers);
return opts;
},
},
};
var gcs = new Storage(authOptions);
gcs.getBuckets(function (err, buckets) {
if (!err) {
buckets.forEach(function (value) {
logger.info(value.id);
});
}
});
var AuthClient = function (client) {
this._client = client;
};
AuthClient.prototype.getClient = function () {
return this._client;
};
var ac = new AuthClient(jwtClient);
const pubsub = new PubSub({
projectId: projectId,
auth: ac,
});
pubsub.getTopics((err, topic) => {
if (err) {
logger.error(err);
return;
}
topic.forEach(function (entry) {
logger.info(entry.name);
});
});
also see google-auth-library-nodejs issu#1210
first set export GOOGLE_APPLICATION_CREDENTIALS=/path/to/svc_account.json
then
using System;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Storage.v1;
using Google.Apis.Pubsub.v1;
using Google.Apis.Services;
using System.Threading.Tasks;
namespace main
{
class Program
{
const string projectID = "your-project-id";
[STAThread]
static void Main(string[] args)
{
new Program().Run().Wait();
}
private Task Run()
{
string[] Scopes = { "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/admin.directory.user.readonly" };
GoogleCredential credential = GoogleCredential.GetApplicationDefault().CreateWithUser("user2@domain.com").CreateScoped(Scopes);
var storageService = new StorageService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "testclient",
});
var request = new BucketsResource.ListRequest(storageService, projectID);
var requestResult = request.Execute();
foreach (var bucket in requestResult.Items)
{
Console.WriteLine(bucket.Name);
}
///
var pubsubService = new PubsubService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "testclient",
});
var prequest = new Google.Apis.Pubsub.v1.ProjectsResource.TopicsResource.ListRequest(pubsubService, "projects/" + projectID);
var prequestResult = prequest.Execute();
foreach (var topic in prequestResult.Topics)
{
Console.WriteLine(topic.Name);
}
return Task.CompletedTask;
}
}
}
Assume both admin@domain.com
and GCE instance running as ${PROJECT_NUMBER}-compute@developer.gserviceaccount.com
has the tokenCreator
role granted on generic-server@${$PROJECT_ID}.iam.gserviceaccount.com
which itself can perform domain-delegation.
Using user_1->impersonate(svc_1)->dwd(user_2)->service
NOTE: to use this mode, the user user_1
as must be able to impersonate svc_1
.
gcloud iam service-accounts add-iam-policy-binding \
svc_1@PROJECT_A.iam.gserviceaccount.com \
--member='user:user1@domain.com' \
--role='roles/iam.serviceAccountTokenCreator'
import google.auth
import time
from google.auth import credentials
from google.cloud import iam_credentials_v1
from google.auth import impersonated_credentials
import google.auth.iam
import urllib.parse
## ******************************************************************************************
## get the source credentials first
source_credentials, project_id = google.auth.default()
project='your-project-id'
target_sa = 'generic-server@pubsub-msg.iam.gserviceaccount.com'
# generate a JWT
sub = "user1@domain.com"
iss = target_sa
target_scopes_for_dwd = "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/admin.directory.user.readonly"
now = int(time.time())
exptime = now + 3600
claim =('{"iss":"%s",'
'"scope":"%s",'
'"aud":"https://accounts.google.com/o/oauth2/token",'
'"sub":"%s",'
'"exp":%s,'
'"iat":%s}') %(iss,target_scopes_for_dwd,sub,exptime,now)
# use the IAM api and the users credentials to sign the JWT
iam_client = iam_credentials_v1.IAMCredentialsClient(credentials=source_credentials)
name = iam_client.service_account_path('-', target_sa)
resp = iam_client.sign_jwt(name=name, delegates=None, payload=claim)
signed_jwt = resp.signed_jwt
print('Signed JWT '+ signed_jwt)
# now exchange the signed_jwt for an access_token representing the subject sub
url = 'https://accounts.google.com/o/oauth2/token'
data = {'grant_type' : 'assertion',
'assertion_type' : 'http://oauth.net/grant_type/jwt/1.0/bearer',
'assertion' : "{}".format(urllib.parse.quote(signed_jwt)) }
headers = {"Content-type": "application/x-www-form-urlencoded"}
resp = requests.post(url, data)
access_token=resp.json()['access_token']
expires_in=resp.json()['expires_in']
token_type=resp.json()['token_type']
# ref # ref https://github.com/googleapis/google-auth-library-python/issues/931
import google.oauth2.credentials
sc = google.oauth2.credentials.Credentials(access_token)
from google.cloud import storage
client = storage.Client(project=project, credentials=sc)
for b in client.list_buckets():
print(b.name)
from google.cloud import pubsub_v1
from google.auth.transport.requests import AuthorizedSession
project_path = f"projects/{project}"
authed_session = AuthorizedSession(sc)
response = authed_session.request('GET', 'https://pubsub.googleapis.com/v1/{}/topics'.format(project_path))
print(response.json())
publisher = pubsub_v1.PublisherClient(credentials=sc)
for topic in publisher.list_topics(request={"project": project_path}):
print(topic.name)
Using svc_gce->impersonate(svc_1)->dwd(user_2)->service
NOTE: to use this mode, the service account svc_gce
the GCE instance runs as must be able to impersonate svc_1
. Or if the GCE VM is running as svc_1
, the svc_1
must be able to “self-impersonate”
gcloud iam service-accounts add-iam-policy-binding \
svc_1@PROJECT_A.iam.gserviceaccount.com \
--member='serviceAccount:svc_gce@PROJECT_B.iam.gserviceaccount.com' \
--role='roles/iam.serviceAccountTokenCreator'
then
gcloud compute ssh instance-1 --project A
$ curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email" --header "Metadata-Flavor: Google"
svc_gce-compute@developer.gserviceaccount.com
The code that you would run is the same as iam_api
Same as user_1->impersonate(svc_1)->dwd(user_2)->service
except this flow uses the impersonated_credential.signer()
This sample uses the same apis as the preceeding snippets in this tab. The only difference is the iamcredentials
api is now wrapped inside impersonated_credentials
import google.auth
import time
from google.auth import credentials
from google.cloud import iam_credentials_v1
from google.auth import impersonated_credentials
import google.auth.iam
import base64
# see https://github.com/googleapis/google-auth-library-python/issues/931
from datetime import datetime, timedelta
from google.auth import _helpers
from google.auth import credentials
from google.auth import exceptions
import requests
# https://google-auth.readthedocs.io/en/master/reference/google.oauth2.service_account.html#domain-wide-delegation
project='your-project-id'
sa_1 = 'generic-server@pubsub-msg.iam.gserviceaccount.com'
## get the source credentials first (this is user_1)
source_credentials, project_id = google.auth.default()
# use that to get sa_1 credentials
target_scopes = ['https://www.googleapis.com/auth/cloud-platform']
sa1_credentials = impersonated_credentials.Credentials(
source_credentials = source_credentials,
target_principal=sa_1,
target_scopes = target_scopes,
delegates=[],
lifetime=500)
### use sa1_credentials to get an token for user2
sub = "user2@domain.com"
target_scopes_for_dwd = "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/admin.directory.user.readonly"
now = int(time.time())
exptime = now + 3600
claim =('{"iss":"%s",'
'"scope":"%s",'
'"aud":"https://accounts.google.com/o/oauth2/token",'
'"sub":"%s",'
'"exp":%s,'
'"iat":%s}') %(sa_1,target_scopes_for_dwd,sub,exptime,now)
# use the impersonated_credentials sign the JWT (since it has a signer)
# https://google-auth.readthedocs.io/en/master/reference/google.auth.impersonated_credentials.html#google.auth.impersonated_credentials.Credentials.signer
## too bad the signer does not return the keyid thats used...
#header = ('{"alg": "RS256", "kid": "%s", "typ": "JWT"}') %("24d9257dc9fe8959728b57655339b753d0f20f09")
header = ('{"alg": "RS256", "typ": "JWT"}')
header_claims = '{}.{}'.format(base64.urlsafe_b64encode(header.encode()).decode().rstrip('='),base64.urlsafe_b64encode(claim.encode()).decode().rstrip('='))
signed_bytes = sa1_credentials.sign_bytes(header_claims.encode())
assertion = header_claims + '.' + base64.urlsafe_b64encode(signed_bytes).decode().rstrip('=')
print('Signed JWT '+ assertion)
# now exchange the signed_jwt for an access_token representing the subject sub
url = 'https://accounts.google.com/o/oauth2/token'
data = {'grant_type' : 'assertion',
'assertion_type' : 'http://oauth.net/grant_type/jwt/1.0/bearer',
'assertion' : assertion }
headers = {"Content-type": "application/x-www-form-urlencoded"}
resp = requests.post(url, data)
## We finally have user2's token....
access_token=resp.json()['access_token']
expires_in=resp.json()['expires_in']
token_type=resp.json()['token_type']
# ref # ref https://github.com/googleapis/google-auth-library-python/issues/931
import google.oauth2.credentials
sc = google.oauth2.credentials.Credentials(access_token)
from google.cloud import storage
client = storage.Client(project=project, credentials=sc)
for b in client.list_buckets():
print(b.name)
from google.cloud import pubsub_v1
from google.auth.transport.requests import AuthorizedSession
project_path = f"projects/{project}"
authed_session = AuthorizedSession(sc)
response = authed_session.request('GET', 'https://pubsub.googleapis.com/v1/{}/topics'.format(project_path))
print(response.json())
publisher = pubsub_v1.PublisherClient(credentials=sc)
for topic in publisher.list_topics(request={"project": project_path}):
print(topic.name)
Please note that the above uses a StaticCredential
which does not exist in google.auth.*
This site supports webmentions. Send me a mention via this form.