Skip to content

Commit

Permalink
feat(impersonated): add impersonated credentials auth (googleapis#1207)
Browse files Browse the repository at this point in the history
  • Loading branch information
bcoe authored and xil222 committed Jul 29, 2021
1 parent b1f1094 commit 59f6017
Show file tree
Hide file tree
Showing 7 changed files with 650 additions and 4 deletions.
70 changes: 70 additions & 0 deletions .readme-partials.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ body: |-
- [JSON Web Tokens](#json-web-tokens) - Use JWT when you are using a single identity for all users. Especially useful for server->server or server->API communication.
- [Google Compute](#compute) - Directly use a service account on Google Cloud Platform. Useful for server->server or server->API communication.
- [Workload Identity Federation](#workload-identity-federation) - Use workload identity federation to access Google Cloud resources from Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC).
- [Impersonated Credentials Client](#impersonated-credentials-client) - access protected resources on behalf of another service account.
## Application Default Credentials
This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs.
Expand Down Expand Up @@ -606,3 +607,72 @@ body: |-
```
A complete example can be found in [`samples/verifyIdToken-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken-iap.js).
## Impersonated Credentials Client
Google Cloud Impersonated credentials used for [Creating short-lived service account credentials](https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials).
Provides authentication for applications where local credentials impersonates a remote service account using [IAM Credentials API](https://cloud.google.com/iam/docs/reference/credentials/rest).
An Impersonated Credentials Client is instantiated with a `sourceClient`. This
client should use credentials that have the "Service Account Token Creator" role (`roles/iam.serviceAccountTokenCreator`),
and should authenticate with the `https://www.googleapis.com/auth/cloud-platform`, or `https://www.googleapis.com/auth/iam` scopes.
`sourceClient` is used by the Impersonated
Credentials Client to impersonate a target service account with a specified
set of scopes.
### Sample Usage
```javascript
const { GoogleAuth, Impersonated } = require('google-auth-library');
const { SecretManagerServiceClient } = require('@google-cloud/secret-manager');
async function main() {
// Acquire source credentials:
const auth = new GoogleAuth();
const client = await auth.getClient();
// Impersonate new credentials:
let targetClient = new Impersonated({
sourceClient: client,
targetPrincipal: 'impersonated-account@projectID.iam.gserviceaccount.com',
lifetime: 30,
delegates: [],
targetScopes: ['https://www.googleapis.com/auth/cloud-platform']
});
// Get impersonated credentials:
const authHeaders = await targetClient.getRequestHeaders();
// Do something with `authHeaders.Authorization`.
// Use impersonated credentials:
const url = 'https://www.googleapis.com/storage/v1/b?project=anotherProjectID'
const resp = await targetClient.request({ url });
for (const bucket of resp.data.items) {
console.log(bucket.name);
}
// Use impersonated credentials with google-cloud client library
// Note: this works only with certain cloud client libraries utilizing gRPC
// e.g., SecretManager, KMS, AIPlatform
// will not currently work with libraries using REST, e.g., Storage, Compute
const smClient = new SecretManagerServiceClient({
projectId: anotherProjectID,
auth: {
getClient: () => targetClient,
},
});
const secretName = 'projects/anotherProjectNumber/secrets/someProjectName/versions/1';
const [accessResponse] = await smClient.accessSecretVersion({
name: secretName,
});
const responsePayload = accessResponse.payload.data.toString('utf8');
// Do something with the secret contained in `responsePayload`.
};
main();
```
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ This library provides a variety of ways to authenticate to your Google services.
- [JSON Web Tokens](#json-web-tokens) - Use JWT when you are using a single identity for all users. Especially useful for server->server or server->API communication.
- [Google Compute](#compute) - Directly use a service account on Google Cloud Platform. Useful for server->server or server->API communication.
- [Workload Identity Federation](#workload-identity-federation) - Use workload identity federation to access Google Cloud resources from Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC).
- [Impersonated Credentials Client](#impersonated-credentials-client) - access protected resources on behalf of another service account.

## Application Default Credentials
This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs.
Expand Down Expand Up @@ -652,6 +653,74 @@ console.log(ticket)

A complete example can be found in [`samples/verifyIdToken-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/verifyIdToken-iap.js).

## Impersonated Credentials Client

Google Cloud Impersonated credentials used for [Creating short-lived service account credentials](https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials).

Provides authentication for applications where local credentials impersonates a remote service account using [IAM Credentials API](https://cloud.google.com/iam/docs/reference/credentials/rest).

An Impersonated Credentials Client is instantiated with a `sourceClient`. This
client should use credentials that have the "Service Account Token Creator" role (`roles/iam.serviceAccountTokenCreator`),
and should authenticate with the `https://www.googleapis.com/auth/cloud-platform`, or `https://www.googleapis.com/auth/iam` scopes.

`sourceClient` is used by the Impersonated
Credentials Client to impersonate a target service account with a specified
set of scopes.

### Sample Usage

```javascript
const { GoogleAuth, Impersonated } = require('google-auth-library');
const { SecretManagerServiceClient } = require('@google-cloud/secret-manager');

async function main() {

// Acquire source credentials:
const auth = new GoogleAuth();
const client = await auth.getClient();

// Impersonate new credentials:
let targetClient = new Impersonated({
sourceClient: client,
targetPrincipal: 'impersonated-account@projectID.iam.gserviceaccount.com',
lifetime: 30,
delegates: [],
targetScopes: ['https://www.googleapis.com/auth/cloud-platform']
});

// Get impersonated credentials:
const authHeaders = await targetClient.getRequestHeaders();
// Do something with `authHeaders.Authorization`.

// Use impersonated credentials:
const url = 'https://www.googleapis.com/storage/v1/b?project=anotherProjectID'
const resp = await targetClient.request({ url });
for (const bucket of resp.data.items) {
console.log(bucket.name);
}

// Use impersonated credentials with google-cloud client library
// Note: this works only with certain cloud client libraries utilizing gRPC
// e.g., SecretManager, KMS, AIPlatform
// will not currently work with libraries using REST, e.g., Storage, Compute
const smClient = new SecretManagerServiceClient({
projectId: anotherProjectID,
auth: {
getClient: () => targetClient,
},
});
const secretName = 'projects/anotherProjectNumber/secrets/someProjectName/versions/1';
const [accessResponse] = await smClient.accessSecretVersion({
name: secretName,
});

const responsePayload = accessResponse.payload.data.toString('utf8');
// Do something with the secret contained in `responsePayload`.
};

main();
```


## Samples

Expand Down
16 changes: 12 additions & 4 deletions src/auth/googleauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {GCPEnv, getEnv} from './envDetect';
import {JWT, JWTOptions} from './jwtclient';
import {Headers, OAuth2ClientOptions, RefreshOptions} from './oauth2client';
import {UserRefreshClient, UserRefreshClientOptions} from './refreshclient';
import {Impersonated, ImpersonatedOptions} from './impersonated';
import {
ExternalAccountClient,
ExternalAccountClientOptions,
Expand All @@ -44,7 +45,11 @@ import {AuthClient} from './authclient';
* Defines all types of explicit clients that are determined via ADC JSON
* config file.
*/
export type JSONClient = JWT | UserRefreshClient | BaseExternalAccountClient;
export type JSONClient =
| JWT
| UserRefreshClient
| BaseExternalAccountClient
| Impersonated;

export interface ProjectIdCallback {
(err?: Error | null, projectId?: string | null): void;
Expand Down Expand Up @@ -86,7 +91,11 @@ export interface GoogleAuthOptions {
/**
* Options object passed to the constructor of the client
*/
clientOptions?: JWTOptions | OAuth2ClientOptions | UserRefreshClientOptions;
clientOptions?:
| JWTOptions
| OAuth2ClientOptions
| UserRefreshClientOptions
| ImpersonatedOptions;

/**
* Required scopes for the desired API request
Expand Down Expand Up @@ -126,14 +135,13 @@ export class GoogleAuth {
// To save the contents of the JSON credential file
jsonContent: JWTInput | ExternalAccountClientOptions | null = null;

cachedCredential: JSONClient | Compute | null = null;
cachedCredential: JSONClient | Impersonated | Compute | null = null;

/**
* Scopes populated by the client library by default. We differentiate between
* these and user defined scopes when deciding whether to use a self-signed JWT.
*/
defaultScopes?: string | string[];

private keyFilename?: string;
private scopes?: string | string[];
private clientOptions?: RefreshOptions;
Expand Down
146 changes: 146 additions & 0 deletions src/auth/impersonated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client';
import {AuthClient} from './authclient';

export interface ImpersonatedOptions extends RefreshOptions {
/**
* Client used to perform exchange for impersonated client.
*/
sourceClient?: AuthClient;
/**
* The service account to impersonate.
*/
targetPrincipal?: string;
/**
* Scopes to request during the authorization grant.
*/
targetScopes?: string[];
/**
* The chained list of delegates required to grant the final access_token.
*/
delegates?: string[];
/**
* Number of seconds the delegated credential should be valid.
*/
lifetime?: number | 3600;
/**
* API endpoint to fetch token from.
*/
endpoint?: string;
}

export interface TokenResponse {
accessToken: string;
expireTime: string;
}

export class Impersonated extends OAuth2Client {
private sourceClient: AuthClient;
private targetPrincipal: string;
private targetScopes: string[];
private delegates: string[];
private lifetime: number;
private endpoint: string;

/**
* Impersonated service account credentials.
*
* Create a new access token by impersonating another service account.
*
* Impersonated Credentials allowing credentials issued to a user or
* service account to impersonate another. The source project using
* Impersonated Credentials must enable the "IAMCredentials" API.
* Also, the target service account must grant the orginating principal
* the "Service Account Token Creator" IAM role.
*
* @param {object} options - The configuration object.
* @param {object} [options.sourceClient] the source credential used as to
* acquire the impersonated credentials.
* @param {string} [options.targetPrincipal] the service account to
* impersonate.
* @param {string[]} [options.delegates] the chained list of delegates
* required to grant the final access_token. If set, the sequence of
* identities must have "Service Account Token Creator" capability granted to
* the preceding identity. For example, if set to [serviceAccountB,
* serviceAccountC], the sourceCredential must have the Token Creator role on
* serviceAccountB. serviceAccountB must have the Token Creator on
* serviceAccountC. Finally, C must have Token Creator on target_principal.
* If left unset, sourceCredential must have that role on targetPrincipal.
* @param {string[]} [options.targetScopes] scopes to request during the
* authorization grant.
* @param {number} [options.lifetime] number of seconds the delegated
* credential should be valid for up to 3600 seconds by default, or 43,200
* seconds by extending the token's lifetime, see:
* https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oauth
* @param {string} [options.endpoint] api endpoint override.
*/
constructor(options: ImpersonatedOptions = {}) {
super(options);
this.credentials = {
expiry_date: 1,
refresh_token: 'impersonated-placeholder',
};
this.sourceClient = options.sourceClient ?? new OAuth2Client();
this.targetPrincipal = options.targetPrincipal ?? '';
this.delegates = options.delegates ?? [];
this.targetScopes = options.targetScopes ?? [];
this.lifetime = options.lifetime ?? 3600;
this.endpoint = options.endpoint ?? 'https://iamcredentials.googleapis.com';
}

/**
* Refreshes the access token.
* @param refreshToken Unused parameter
*/
protected async refreshToken(
refreshToken?: string | null
): Promise<GetTokenResponse> {
try {
await this.sourceClient.getAccessToken();
const name = 'projects/-/serviceAccounts/' + this.targetPrincipal;
const u = `${this.endpoint}/v1/${name}:generateAccessToken`;
const body = {
delegates: this.delegates,
scope: this.targetScopes,
lifetime: this.lifetime + 's',
};
const res = await this.sourceClient.request<TokenResponse>({
url: u,
data: body,
method: 'POST',
});
const tokenResponse = res.data;
this.credentials.access_token = tokenResponse.accessToken;
this.credentials.expiry_date = Date.parse(tokenResponse.expireTime);
return {
tokens: this.credentials,
res,
};
} catch (error) {
const status = error?.response?.data?.error?.status;
const message = error?.response?.data?.error?.message;
if (status && message) {
error.message = `${status}: unable to impersonate: ${message}`;
throw error;
} else {
error.message = `unable to impersonate: ${error}`;
throw error;
}
}
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export {IAMAuth, RequestMetadata} from './auth/iam';
export {IdTokenClient, IdTokenProvider} from './auth/idtokenclient';
export {Claims, JWTAccess} from './auth/jwtaccess';
export {JWT, JWTOptions} from './auth/jwtclient';
export {Impersonated, ImpersonatedOptions} from './auth/impersonated';
export {
Certificates,
CodeChallengeMethod,
Expand Down

0 comments on commit 59f6017

Please sign in to comment.