Skip to content

Commit

Permalink
feat: Support instantiating GoogleAuth with an AuthClient (#1364)
Browse files Browse the repository at this point in the history
  • Loading branch information
d-goog authored Feb 17, 2022
1 parent 8c2c355 commit 8839b5b
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 69 deletions.
31 changes: 9 additions & 22 deletions .readme-partials.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ body: |-
- `$POOL_ID`: The workload identity pool ID.
- `$OIDC_PROVIDER_ID`: The OIDC provider ID.
- `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate.
- `$PATH_TO_OIDC_ID_TOKEN`: The file path where the OIDC token will be retrieved from.
- `$PATH_TO_OIDC_ID_TOKEN`: The file path where the OIDC token will be retrieved from.
This will generate the configuration file in the specified output file.
Expand Down Expand Up @@ -618,7 +618,7 @@ body: |-
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.
Expand Down Expand Up @@ -656,7 +656,7 @@ body: |-
}
// Use impersonated credentials with google-cloud client library
// Note: this works only with certain cloud client libraries utilizing gRPC
// 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({
Expand All @@ -681,14 +681,14 @@ body: |-
[Downscoping with Credential Access Boundaries](https://cloud.google.com/iam/docs/downscoping-short-lived-credentials) is used to restrict the Identity and Access Management (IAM) permissions that a short-lived credential can use.
The `DownscopedClient` class can be used to produce a downscoped access token from a
The `DownscopedClient` class can be used to produce a downscoped access token from a
`CredentialAccessBoundary` and a source credential. The Credential Access Boundary specifies which resources the newly created credential can access, as well as an upper bound on the permissions that are available on each resource. Using downscoped credentials ensures tokens in flight always have the least privileges, e.g. Principle of Least Privilege.
> Notice: Only Cloud Storage supports Credential Access Boundaries for now.
### Sample Usage
There are two entities needed to generate and use credentials generated from
Downscoped Client with Credential Access Boundaries.
Downscoped Client with Credential Access Boundaries.
- Token broker: This is the entity with elevated permissions. This entity has the permissions needed to generate downscoped tokens. The common pattern of usage is to have a token broker with elevated access generate these downscoped credentials from higher access source credentials and pass the downscoped short-lived access tokens to a token consumer via some secure authenticated channel for limited access to Google Cloud Storage resources.
Expand Down Expand Up @@ -731,10 +731,10 @@ body: |-
expiry_date = refreshedAccessToken.expirationTime;
```
A token broker can be set up on a server in a private network. Various workloads
A token broker can be set up on a server in a private network. Various workloads
(token consumers) in the same network will send authenticated requests to that broker for downscoped tokens to access or modify specific google cloud storage buckets.
The broker will instantiate downscoped credentials instances that can be used to generate short lived downscoped access tokens which will be passed to the token consumer.
The broker will instantiate downscoped credentials instances that can be used to generate short lived downscoped access tokens which will be passed to the token consumer.
- Token consumer: This is the consumer of the downscoped tokens. This entity does not have the direct ability to generate access tokens and instead relies on the token broker to provide it with downscoped tokens to run operations on GCS buckets. It is assumed that the downscoped token consumer may have its own mechanism to authenticate itself with the token broker.
Expand Down Expand Up @@ -763,20 +763,7 @@ body: |-
// Use the consumer client to define storageOptions and create a GCS object.
const storageOptions = {
projectId: 'my_project_id',
authClient: {
sign: () => Promise.reject('unsupported'),
getCredentials: () => Promise.reject(),
request: (opts, callback) => {
return oauth2Client.request(opts, callback);
},
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;
},
},
authClient: oauth2Client,
};
const storage = new Storage(storageOptions);
Expand All @@ -788,4 +775,4 @@ body: |-
console.log(downloadFile.toString('utf8'));
main().catch(console.error);
```
```
27 changes: 7 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ Where the following variables need to be substituted:
- `$POOL_ID`: The workload identity pool ID.
- `$OIDC_PROVIDER_ID`: The OIDC provider ID.
- `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate.
- `$PATH_TO_OIDC_ID_TOKEN`: The file path where the OIDC token will be retrieved from.
- `$PATH_TO_OIDC_ID_TOKEN`: The file path where the OIDC token will be retrieved from.

This will generate the configuration file in the specified output file.

Expand Down Expand Up @@ -700,7 +700,7 @@ async function main() {
}

// Use impersonated credentials with google-cloud client library
// Note: this works only with certain cloud client libraries utilizing gRPC
// 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({
Expand All @@ -725,14 +725,14 @@ main();

[Downscoping with Credential Access Boundaries](https://cloud.google.com/iam/docs/downscoping-short-lived-credentials) is used to restrict the Identity and Access Management (IAM) permissions that a short-lived credential can use.

The `DownscopedClient` class can be used to produce a downscoped access token from a
The `DownscopedClient` class can be used to produce a downscoped access token from a
`CredentialAccessBoundary` and a source credential. The Credential Access Boundary specifies which resources the newly created credential can access, as well as an upper bound on the permissions that are available on each resource. Using downscoped credentials ensures tokens in flight always have the least privileges, e.g. Principle of Least Privilege.

> Notice: Only Cloud Storage supports Credential Access Boundaries for now.
### Sample Usage
There are two entities needed to generate and use credentials generated from
Downscoped Client with Credential Access Boundaries.
Downscoped Client with Credential Access Boundaries.

- Token broker: This is the entity with elevated permissions. This entity has the permissions needed to generate downscoped tokens. The common pattern of usage is to have a token broker with elevated access generate these downscoped credentials from higher access source credentials and pass the downscoped short-lived access tokens to a token consumer via some secure authenticated channel for limited access to Google Cloud Storage resources.

Expand Down Expand Up @@ -775,10 +775,10 @@ access_token = refreshedAccessToken.token;
expiry_date = refreshedAccessToken.expirationTime;
```

A token broker can be set up on a server in a private network. Various workloads
A token broker can be set up on a server in a private network. Various workloads
(token consumers) in the same network will send authenticated requests to that broker for downscoped tokens to access or modify specific google cloud storage buckets.

The broker will instantiate downscoped credentials instances that can be used to generate short lived downscoped access tokens which will be passed to the token consumer.
The broker will instantiate downscoped credentials instances that can be used to generate short lived downscoped access tokens which will be passed to the token consumer.

- Token consumer: This is the consumer of the downscoped tokens. This entity does not have the direct ability to generate access tokens and instead relies on the token broker to provide it with downscoped tokens to run operations on GCS buckets. It is assumed that the downscoped token consumer may have its own mechanism to authenticate itself with the token broker.

Expand Down Expand Up @@ -807,20 +807,7 @@ oauth2Client.refreshHandler = async () => {
// Use the consumer client to define storageOptions and create a GCS object.
const storageOptions = {
projectId: 'my_project_id',
authClient: {
sign: () => Promise.reject('unsupported'),
getCredentials: () => Promise.reject(),
request: (opts, callback) => {
return oauth2Client.request(opts, callback);
},
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;
},
},
authClient: oauth2Client,
};

const storage = new Storage(storageOptions);
Expand Down
24 changes: 5 additions & 19 deletions samples/downscopedclient.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ async function main() {
},
};

const oauth2Client = new OAuth2Client();
const googleAuth = new GoogleAuth({
scopes: 'https://www.googleapis.com/auth/cloud-platform',
});
Expand All @@ -63,6 +62,9 @@ async function main() {
const client = await googleAuth.getClient();
// Use the client to generate a DownscopedClient.
const cabClient = new DownscopedClient(client, cab);

// OAuth 2.0 Client
const oauth2Client = new OAuth2Client();
// Define a refreshHandler that will be used to refresh the downscoped token
// when it expires.
oauth2Client.refreshHandler = async () => {
Expand All @@ -75,31 +77,15 @@ async function main() {

const storageOptions = {
projectId,
authClient: {
getCredentials: async () => {
Promise.reject();
},
request: opts => {
return oauth2Client.request(opts);
},
sign: () => {
Promise.reject('unsupported');
},
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;
},
},
authClient: new GoogleAuth({auth: oauth2Client}),
};

const storage = new Storage(storageOptions);
const downloadFile = await storage
.bucket(bucketName)
.file(objectName)
.download();
console.log('Successfully retrieved file. Contents:');
console.log(downloadFile.toString('utf8'));
}

Expand Down
6 changes: 4 additions & 2 deletions samples/test/downscoping-with-cab.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ const {promisify} = require('util');
const exec = promisify(cp.exec);
// Copy values from the output of samples/scripts/downscoping-with-cab-setup.js.
// GCS bucket name.
const bucketName = 'cab-int-bucket-z2zsauf4sj';
const bucketName = 'cab-int-bucket-brd3qlsuok';
// GCS object name.
const objectName = 'cab-first-z2zsauf4sj.txt';
const objectName = 'cab-first-"brd3qlsuok.txt';

/**
* Runs the provided command using asynchronous child_process.exec.
Expand All @@ -59,7 +59,9 @@ describe('samples for downscoping with cab', () => {
OBJECT_NAME: objectName,
},
});

// Confirm expected script output.
assert.match(output, /Successfully retrieved file/);
assert.match(output, /first/);
});
});
28 changes: 22 additions & 6 deletions src/auth/googleauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export interface ADCResponse {
}

export interface GoogleAuthOptions {
/**
* An `AuthClient` to use
*/
auth?: AuthClient;
/**
* Path to a .json, .pem, or .p12 key file
*/
Expand Down Expand Up @@ -135,7 +139,8 @@ export class GoogleAuth {
// To save the contents of the JSON credential file
jsonContent: JWTInput | ExternalAccountClientOptions | null = null;

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

/**
* Scopes populated by the client library by default. We differentiate between
Expand All @@ -153,7 +158,9 @@ export class GoogleAuth {

constructor(opts?: GoogleAuthOptions) {
opts = opts || {};

this._cachedProjectId = opts.projectId || null;
this.cachedCredential = opts.auth || null;
this.keyFilename = opts.keyFilename || opts.keyFile;
this.scopes = opts.scopes;
this.jsonContent = opts.credentials || null;
Expand Down Expand Up @@ -270,7 +277,7 @@ export class GoogleAuth {
// If we've already got a cached credential, just return it.
if (this.cachedCredential) {
return {
credential: this.cachedCredential as JSONClient,
credential: this.cachedCredential,
projectId: await this.getProjectIdAsync(),
};
}
Expand Down Expand Up @@ -313,7 +320,10 @@ export class GoogleAuth {
try {
isGCE = await this._checkIsGCE();
} catch (e) {
e.message = `Unexpected error determining execution environment: ${e.message}`;
if (e instanceof Error) {
e.message = `Unexpected error determining execution environment: ${e.message}`;
}

throw e;
}

Expand Down Expand Up @@ -364,7 +374,10 @@ export class GoogleAuth {
options
);
} catch (e) {
e.message = `Unable to read the credential file specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable: ${e.message}`;
if (e instanceof Error) {
e.message = `Unable to read the credential file specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable: ${e.message}`;
}

throw e;
}
}
Expand Down Expand Up @@ -438,7 +451,10 @@ export class GoogleAuth {
throw new Error();
}
} catch (err) {
err.message = `The file at ${filePath} does not exist, or it is not a file. ${err.message}`;
if (err instanceof Error) {
err.message = `The file at ${filePath} does not exist, or it is not a file. ${err.message}`;
}

throw err;
}

Expand Down Expand Up @@ -511,7 +527,7 @@ export class GoogleAuth {
// cache both raw data used to instantiate client and client itself.
this.jsonContent = json;
this.cachedCredential = client;
return this.cachedCredential;
return client;
}

/**
Expand Down
29 changes: 29 additions & 0 deletions test/test.googleauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,35 @@ describe('googleauth', () => {
return sandbox.stub(process, 'env').value(envVars);
}

it('should accept and use an `AuthClient`', async () => {
const customRequestHeaders = {
'my-unique': 'header',
};

// Using a custom `AuthClient` to ensure any `AuthClient` would work
class MyAuthClient extends AuthClient {
async getAccessToken() {
return {token: '', res: undefined};
}

async getRequestHeaders() {
return {...customRequestHeaders};
}

request = OAuth2Client.prototype.request.bind(this);
}

const authClient = new MyAuthClient();

const auth = new GoogleAuth({
auth: authClient,
});

assert.equal(auth.cachedCredential, authClient);
assert.equal(await auth.getClient(), authClient);
assert.deepEqual(await auth.getRequestHeaders(''), customRequestHeaders);
});

it('fromJSON should support the instantiated named export', () => {
const result = auth.fromJSON(createJwtJSON());
assert(result);
Expand Down

0 comments on commit 8839b5b

Please sign in to comment.