Skip to content

Commit cb78a1b

Browse files
salrashid123d-googgcf-owl-bot[bot]
authored
feat: Add impersonated signer (googleapis#1694)
* adding fixes * fix sample * return signedblobresponse except for gcs clients Signed-off-by: salrashid123 <[email protected]> * update test * fixes * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Signed-off-by: salrashid123 <[email protected]> Co-authored-by: Daniel Bankhead <[email protected]> Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent a735ec5 commit cb78a1b

File tree

7 files changed

+231
-0
lines changed

7 files changed

+231
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -1217,6 +1217,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/google-auth-librar
12171217
| Oauth2-code Verifier | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/oauth2-codeVerifier.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/oauth2-codeVerifier.js,samples/README.md) |
12181218
| Oauth2 | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/oauth2.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/oauth2.js,samples/README.md) |
12191219
| Sign Blob | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/signBlob.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/signBlob.js,samples/README.md) |
1220+
| Sign Blob Impersonated | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/signBlobImpersonated.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/signBlobImpersonated.js,samples/README.md) |
12201221
| Verify Google Id Token | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyGoogleIdToken.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyGoogleIdToken.js,samples/README.md) |
12211222
| Verifying ID Tokens from Identity-Aware Proxy (IAP) | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyIdToken-iap.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyIdToken-iap.js,samples/README.md) |
12221223
| Verify Id Token | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyIdToken.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/verifyIdToken.js,samples/README.md) |

samples/README.md

+18
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ This is Google's officially supported [node.js](http://nodejs.org/) client libra
3030
* [Oauth2-code Verifier](#oauth2-code-verifier)
3131
* [Oauth2](#oauth2)
3232
* [Sign Blob](#sign-blob)
33+
* [Sign Blob Impersonated](#sign-blob-impersonated)
3334
* [Verify Google Id Token](#verify-google-id-token)
3435
* [Verifying ID Tokens from Identity-Aware Proxy (IAP)](#verifying-id-tokens-from-identity-aware-proxy-iap)
3536
* [Verify Id Token](#verify-id-token)
@@ -359,6 +360,23 @@ __Usage:__
359360

360361

361362

363+
### Sign Blob Impersonated
364+
365+
View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/signBlobImpersonated.js).
366+
367+
[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/signBlobImpersonated.js,samples/README.md)
368+
369+
__Usage:__
370+
371+
372+
`node samples/signBlobImpersonated.js`
373+
374+
375+
-----
376+
377+
378+
379+
362380
### Verify Google Id Token
363381

364382
View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/verifyGoogleIdToken.js).

samples/signBlobImpersonated.js

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2023 Google LLC
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
'use strict';
15+
16+
const {GoogleAuth, Impersonated} = require('google-auth-library');
17+
18+
/**
19+
* Use the iamcredentials API to sign a blob of data.
20+
*/
21+
async function main() {
22+
// get source credentials
23+
const auth = new GoogleAuth();
24+
const client = await auth.getClient();
25+
26+
// First impersonate
27+
const scopes = ['https://www.googleapis.com/auth/cloud-platform'];
28+
29+
const targetPrincipal = '[email protected]';
30+
const targetClient = new Impersonated({
31+
sourceClient: client,
32+
targetPrincipal: targetPrincipal,
33+
lifetime: 30,
34+
delegates: [],
35+
targetScopes: [scopes],
36+
});
37+
38+
const signedData = await targetClient.sign('some data');
39+
console.log(signedData.signedBlob);
40+
41+
// or use the client to create a GCS signedURL
42+
// const { Storage } = require('@google-cloud/storage');
43+
44+
// const projectId = 'yourProjectID'
45+
// const bucketName = 'yourBucket'
46+
// const objectName = 'yourObject'
47+
48+
// // use the impersonated client to access gcs
49+
// const storageOptions = {
50+
// projectId,
51+
// authClient: targetClient,
52+
// };
53+
54+
// const storage = new Storage(storageOptions);
55+
56+
// const signOptions = {
57+
// version: 'v4',
58+
// action: 'read',
59+
// expires: Date.now() + 15 * 60 * 1000, // 15 minutes
60+
// };
61+
62+
// const signedURL = await storage
63+
// .bucket(bucketName)
64+
// .file(objectName)
65+
// .getSignedUrl(signOptions);
66+
67+
// console.log(signedURL);
68+
}
69+
70+
main().catch(e => {
71+
console.error(e);
72+
throw e;
73+
});

src/auth/googleauth.ts

+10
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,10 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {
933933
private async getCredentialsAsync(): Promise<CredentialBody> {
934934
const client = await this.getClient();
935935

936+
if (client instanceof Impersonated) {
937+
return {client_email: client.getTargetPrincipal()};
938+
}
939+
936940
if (client instanceof BaseExternalAccountClient) {
937941
const serviceAccountEmail = client.getServiceAccountEmail();
938942
if (serviceAccountEmail) {
@@ -1059,6 +1063,12 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {
10591063
*/
10601064
async sign(data: string): Promise<string> {
10611065
const client = await this.getClient();
1066+
1067+
if (client instanceof Impersonated) {
1068+
const signed = await client.sign(data);
1069+
return signed.signedBlob;
1070+
}
1071+
10621072
const crypto = createCrypto();
10631073
if (client instanceof JWT && client.key) {
10641074
const sign = await crypto.sign(client.key, data);

src/auth/impersonated.ts

+29
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import {AuthClient} from './authclient';
2323
import {IdTokenProvider} from './idtokenclient';
2424
import {GaxiosError} from 'gaxios';
25+
import {SignBlobResponse} from './googleauth';
2526

2627
export interface ImpersonatedOptions extends OAuth2ClientOptions {
2728
/**
@@ -126,6 +127,34 @@ export class Impersonated extends OAuth2Client implements IdTokenProvider {
126127
this.endpoint = options.endpoint ?? 'https://iamcredentials.googleapis.com';
127128
}
128129

130+
/**
131+
* Signs some bytes.
132+
*
133+
* {@link https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signBlob Reference Documentation}
134+
* @param blobToSign String to sign.
135+
* @return <SignBlobResponse> denoting the keyyID and signedBlob in base64 string
136+
*/
137+
async sign(blobToSign: string): Promise<SignBlobResponse> {
138+
await this.sourceClient.getAccessToken();
139+
const name = `projects/-/serviceAccounts/${this.targetPrincipal}`;
140+
const u = `${this.endpoint}/v1/${name}:signBlob`;
141+
const body = {
142+
delegates: this.delegates,
143+
payload: Buffer.from(blobToSign).toString('base64'),
144+
};
145+
const res = await this.sourceClient.request<SignBlobResponse>({
146+
url: u,
147+
data: body,
148+
method: 'POST',
149+
});
150+
return res.data;
151+
}
152+
153+
/** The service account email to be impersonated. */
154+
getTargetPrincipal(): string {
155+
return this.targetPrincipal;
156+
}
157+
129158
/**
130159
* Refreshes the access token.
131160
* @param refreshToken Unused parameter

test/test.googleauth.ts

+56
Original file line numberDiff line numberDiff line change
@@ -1578,6 +1578,62 @@ describe('googleauth', () => {
15781578
.post('/token')
15791579
.reply(200, {});
15801580
}
1581+
describe('for impersonated types', () => {
1582+
describe('for impersonated credentials signing', () => {
1583+
const now = new Date().getTime();
1584+
const saSuccessResponse = {
1585+
accessToken: 'SA_ACCESS_TOKEN',
1586+
expireTime: new Date(now + 3600 * 1000).toISOString(),
1587+
};
1588+
1589+
it('should use IAMCredentials signBlob endpoint when impersonation is used', async () => {
1590+
// Set up a mock to return path to a valid credentials file.
1591+
mockEnvVar(
1592+
'GOOGLE_APPLICATION_CREDENTIALS',
1593+
'./test/fixtures/impersonated_application_default_credentials.json'
1594+
);
1595+
1596+
// Set up a mock to explicity return the Project ID, as needed for impersonated ADC
1597+
mockEnvVar('GCLOUD_PROJECT', STUB_PROJECT);
1598+
1599+
const auth = new GoogleAuth();
1600+
const client = await auth.getClient();
1601+
1602+
const email = '[email protected]';
1603+
const iamUri = 'https://iamcredentials.googleapis.com';
1604+
const iamPath = `/v1/projects/-/serviceAccounts/${email}:signBlob`;
1605+
const signedBlob = 'erutangis';
1606+
const keyId = '12345';
1607+
const data = 'abc123';
1608+
const scopes = [
1609+
nock('https://oauth2.googleapis.com').post('/token').reply(200, {
1610+
access_token: saSuccessResponse.accessToken,
1611+
}),
1612+
nock(iamUri)
1613+
.post(
1614+
iamPath,
1615+
{
1616+
delegates: [],
1617+
payload: Buffer.from(data, 'utf-8').toString('base64'),
1618+
},
1619+
{
1620+
reqheaders: {
1621+
Authorization: `Bearer ${saSuccessResponse.accessToken}`,
1622+
'Content-Type': 'application/json',
1623+
},
1624+
}
1625+
)
1626+
.reply(200, {keyId: keyId, signedBlob: signedBlob}),
1627+
];
1628+
1629+
const signed = await auth.sign(data);
1630+
1631+
scopes.forEach(x => x.done());
1632+
assert(client instanceof Impersonated);
1633+
assert.strictEqual(signed, signedBlob);
1634+
});
1635+
});
1636+
});
15811637

15821638
describe('for external_account types', () => {
15831639
let fromJsonSpy: sinon.SinonSpy<

test/test.impersonated.ts

+44
Original file line numberDiff line numberDiff line change
@@ -455,4 +455,48 @@ describe('impersonated', () => {
455455

456456
scopes.forEach(s => s.done());
457457
});
458+
459+
it('should sign a blob', async () => {
460+
const expectedKeyID = '12345';
461+
const expectedSignedBlob = 'signed';
462+
const expectedBlobToSign = 'signme';
463+
const expectedDeligates = ['deligate-1', 'deligate-2'];
464+
const email = '[email protected]';
465+
466+
const scopes = [
467+
createGTokenMock({
468+
access_token: 'abc123',
469+
}),
470+
nock('https://iamcredentials.googleapis.com')
471+
.post(
472+
`/v1/projects/-/serviceAccounts/${email}:signBlob`,
473+
(body: {delegates: string[]; payload: string}) => {
474+
assert.strictEqual(
475+
body.payload,
476+
Buffer.from(expectedBlobToSign).toString('base64')
477+
);
478+
assert.deepStrictEqual(body.delegates, expectedDeligates);
479+
return true;
480+
}
481+
)
482+
.reply(200, {
483+
keyId: expectedKeyID,
484+
signedBlob: expectedSignedBlob,
485+
}),
486+
];
487+
488+
const impersonated = new Impersonated({
489+
sourceClient: createSampleJWTClient(),
490+
targetPrincipal: email,
491+
lifetime: 30,
492+
delegates: expectedDeligates,
493+
targetScopes: ['https://www.googleapis.com/auth/cloud-platform'],
494+
});
495+
496+
const resp = await impersonated.sign(expectedBlobToSign);
497+
assert.equal(email, impersonated.getTargetPrincipal());
498+
assert.equal(resp.keyId, expectedKeyID);
499+
assert.equal(resp.signedBlob, expectedSignedBlob);
500+
scopes.forEach(s => s.done());
501+
});
458502
});

0 commit comments

Comments
 (0)