Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(impersonated): add impersonated credentials auth #1207

Merged
merged 31 commits into from
Jul 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9ca71b8
Add impersonated credentials
salrashid123 Aug 29, 2019
9bd3590
feat(impersonated): Add Impersonated Credentials
salrashid123 Aug 29, 2019
b18dfc2
fix linter stuff
salrashid123 Aug 29, 2019
f91367c
Merge branch 'master' into master
bcoe Sep 5, 2019
c6e7c72
add endpoints override
salrashid123 Sep 7, 2019
706a473
Merge branch 'master' of https://github.com/salrashid123/google-auth-…
salrashid123 Sep 7, 2019
3caf0cb
Merge branch 'master' into master
bcoe Oct 14, 2019
fda2bab
Merge branch 'master' into master
bcoe Jan 16, 2020
30e81b4
Merge branch 'master' into master
bcoe Apr 27, 2020
c904057
Merge branch 'master' into master
bcoe May 21, 2020
28a448a
Merge branch 'master' into master
bcoe May 27, 2021
419a946
Merge branch 'master' into master
bcoe Jun 25, 2021
cf3b318
Merge branch 'master' into master
bcoe Jul 7, 2021
9b56c6c
test: make more idiomatic, cleanup tests
bcoe Jul 7, 2021
c6012bc
add tests for impersonated
bcoe Jul 16, 2021
1794dc5
Merge branch 'master' into impersonated
bcoe Jul 21, 2021
70f0821
test: increase coverage
bcoe Jul 21, 2021
c4e6310
update README
bcoe Jul 21, 2021
f53cb5b
update README
bcoe Jul 21, 2021
910c18c
slight copy edits
bcoe Jul 21, 2021
728573f
🦉 Updates from OwlBot
gcf-owl-bot[bot] Jul 21, 2021
180fdf5
chore: address review
bcoe Jul 22, 2021
24d2f42
chore: address review
bcoe Jul 22, 2021
e66f7ce
chore: address code review
bcoe Jul 22, 2021
8487662
chore: add comments
bcoe Jul 22, 2021
48fe1db
docs: fix example
bcoe Jul 22, 2021
e4f5917
chore: address code review
bcoe Jul 27, 2021
6cb0e20
chore: address additional review
bcoe Jul 27, 2021
2cf98e9
Merge branch 'master' into impersonated
bcoe Jul 28, 2021
a9752ee
🦉 Updates from OwlBot
gcf-owl-bot[bot] Jul 28, 2021
aa2178e
chore: address additional code review
bcoe Jul 29, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
bcoe marked this conversation as resolved.
Show resolved Hide resolved
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
bcoe marked this conversation as resolved.
Show resolved Hide resolved
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;
bcoe marked this conversation as resolved.
Show resolved Hide resolved

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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to add a test for Impersonated:

it('should export all the things', () => {

export {
Certificates,
CodeChallengeMethod,
Expand Down
Loading