Skip to content

Commit

Permalink
feat: Extend API Key Support (#1835)
Browse files Browse the repository at this point in the history
* feat: Extend API Key Support

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* feat: Support `apiKey` as an ADC fallback

* refactor: Move `apiKey` to base client options

* docs: clarity

* refactor: API Key Support

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* fix: type

* feat: Export Error Messages

* test: Add tests for API Key Support

* test: cleanup

* docs: Clarifications

* refactor: streamline

* chore: merge cleanup

* docs(sample): Add API Key Sample

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* chore: OCD

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* Apply suggestions from code review

Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com>

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

* 🦉 Updates from OwlBot post-processor

See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md

---------

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
Co-authored-by: sofisl <55454395+sofisl@users.noreply.github.com>
  • Loading branch information
3 people committed Aug 19, 2024
1 parent e9459f3 commit 5fc3bcc
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 50 deletions.
22 changes: 22 additions & 0 deletions .readme-partials.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,28 @@ body: |-
This method will throw if the token is invalid.
#### Using an API Key
An API key can be provided to the constructor:
```js
const client = new OAuth2Client({
apiKey: 'my-api-key'
});
```
Note, classes that extend from this can utilize this parameter as well, such as `JWT` and `UserRefreshClient`.
Additionally, an API key can be used in `GoogleAuth` via the `clientOptions` parameter and will be passed to any generated `OAuth2Client` instances:
```js
const auth = new GoogleAuth({
clientOptions: {
apiKey: 'my-api-key'
}
})
```
API Key support varies by API.
## JSON Web Tokens
The Google Developers Console provides a `.json` file that you can use to configure a JWT auth client and authenticate your requests, for example when using a service account.
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,28 @@ console.log(tokenInfo.scopes);

This method will throw if the token is invalid.

#### Using an API Key

An API key can be provided to the constructor:
```js
const client = new OAuth2Client({
apiKey: 'my-api-key'
});
```

Note, classes that extend from this can utilize this parameter as well, such as `JWT` and `UserRefreshClient`.

Additionally, an API key can be used in `GoogleAuth` via the `clientOptions` parameter and will be passed to any generated `OAuth2Client` instances:
```js
const auth = new GoogleAuth({
clientOptions: {
apiKey: 'my-api-key'
}
})
```

API Key support varies by API.

## JSON Web Tokens
The Google Developers Console provides a `.json` file that you can use to configure a JWT auth client and authenticate your requests, for example when using a service account.

Expand Down Expand Up @@ -1326,6 +1348,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/google-auth-librar
| Sample | Source Code | Try it |
| --------------------------- | --------------------------------- | ------ |
| Adc | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/adc.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/adc.js,samples/README.md) |
| Authenticate API Key | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/authenticateAPIKey.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/authenticateAPIKey.js,samples/README.md) |
| Authenticate Explicit | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/authenticateExplicit.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/authenticateExplicit.js,samples/README.md) |
| Authenticate Implicit With Adc | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/authenticateImplicitWithAdc.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/authenticateImplicitWithAdc.js,samples/README.md) |
| Compute | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/compute.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/compute.js,samples/README.md) |
Expand Down
18 changes: 18 additions & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This is Google's officially supported [node.js](http://nodejs.org/) client libra
* [Before you begin](#before-you-begin)
* [Samples](#samples)
* [Adc](#adc)
* [Authenticate API Key](#authenticate-api-key)
* [Authenticate Explicit](#authenticate-explicit)
* [Authenticate Implicit With Adc](#authenticate-implicit-with-adc)
* [Compute](#compute)
Expand Down Expand Up @@ -67,6 +68,23 @@ __Usage:__



### Authenticate API Key

View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/authenticateAPIKey.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/authenticateAPIKey.js,samples/README.md)

__Usage:__


`node samples/authenticateAPIKey.js`


-----




### Authenticate Explicit

View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/authenticateExplicit.js).
Expand Down
63 changes: 63 additions & 0 deletions samples/authenticateAPIKey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2024 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.

/**
* Lists storage buckets by authenticating with ADC.
*/
function main() {
// [START apikeys_authenticate_api_key]

const {
v1: {LanguageServiceClient},
} = require('@google-cloud/language');

/**
* Authenticates with an API key for Google Language service.
*
* @param {string} apiKey An API Key to use
*/
async function authenticateWithAPIKey(apiKey) {
const language = new LanguageServiceClient({apiKey});

// Alternatively:
// const auth = new GoogleAuth({apiKey});
// const {GoogleAuth} = require('google-auth-library');
// const language = new LanguageServiceClient({auth});

const text = 'Hello, world!';

const [response] = await language.analyzeSentiment({
document: {
content: text,
type: 'PLAIN_TEXT',
},
});

console.log(`Text: ${text}`);
console.log(
`Sentiment: ${response.documentSentiment.score}, ${response.documentSentiment.magnitude}`
);
console.log('Successfully authenticated using the API key');
}

authenticateWithAPIKey();
// [END apikeys_authenticate_api_key]
}

process.on('unhandledRejection', err => {
console.error(err.message);
process.exitCode = 1;
});

main(...process.argv.slice(2));
1 change: 1 addition & 0 deletions samples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
},
"license": "Apache-2.0",
"dependencies": {
"@google-cloud/language": "^6.5.0",
"@google-cloud/storage": "^7.0.0",
"@googleapis/iam": "^21.0.0",
"google-auth-library": "^9.13.0",
Expand Down
6 changes: 6 additions & 0 deletions src/auth/authclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ interface AuthJSONOptions {
*/
export interface AuthClientOptions
extends Partial<OriginalAndCamel<AuthJSONOptions>> {
/**
* An API key to use, optional.
*/
apiKey?: string;
credentials?: Credentials;

/**
Expand Down Expand Up @@ -170,6 +174,7 @@ export abstract class AuthClient
extends EventEmitter
implements CredentialsClient
{
apiKey?: string;
projectId?: string | null;
/**
* The quota project ID. The quota project can be used by client libraries for the billing purpose.
Expand All @@ -188,6 +193,7 @@ export abstract class AuthClient
const options = originalOrCamelOptions(opts);

// Shared auth options
this.apiKey = opts.apiKey;
this.projectId = options.get('project_id') ?? null;
this.quotaProjectId = options.get('quota_project_id');
this.credentials = options.get('credentials') ?? {};
Expand Down
93 changes: 54 additions & 39 deletions src/auth/googleauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ export interface ADCResponse {
}

export interface GoogleAuthOptions<T extends AuthClient = JSONClient> {
/**
* An API key to use, optional. Cannot be used with {@link GoogleAuthOptions.credentials `credentials`}.
*/
apiKey?: string;

/**
* An `AuthClient` to use
*/
Expand All @@ -102,6 +107,7 @@ export interface GoogleAuthOptions<T extends AuthClient = JSONClient> {
/**
* Object containing client_email and private_key properties, or the
* external account client options.
* Cannot be used with {@link GoogleAuthOptions.apiKey `apiKey`}.
*/
credentials?: JWTInput | ExternalAccountClientOptions;

Expand Down Expand Up @@ -136,7 +142,9 @@ export interface GoogleAuthOptions<T extends AuthClient = JSONClient> {
export const CLOUD_SDK_CLIENT_ID =
'764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com';

const GoogleAuthExceptionMessages = {
export const GoogleAuthExceptionMessages = {
API_KEY_WITH_CREDENTIALS:
'API Keys and Credentials are mutually exclusive authentication methods and cannot be used together.',
NO_PROJECT_ID_FOUND:
'Unable to detect a Project Id in the current environment. \n' +
'To learn more about authentication and Google APIs, visit: \n' +
Expand All @@ -145,6 +153,8 @@ const GoogleAuthExceptionMessages = {
'Unable to find credentials in current environment. \n' +
'To learn more about authentication and Google APIs, visit: \n' +
'https://cloud.google.com/docs/authentication/getting-started',
NO_ADC_FOUND:
'Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.',
NO_UNIVERSE_DOMAIN_FOUND:
'Unable to detect a Universe Domain in the current environment.\n' +
'To learn more about Universe Domain retrieval, visit: \n' +
Expand Down Expand Up @@ -174,6 +184,7 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {

// To save the contents of the JSON credential file
jsonContent: JWTInput | ExternalAccountClientOptions | null = null;
apiKey: string | null;

cachedCredential: AnyAuthClient | T | null = null;

Expand Down Expand Up @@ -202,15 +213,21 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {
*
* @param opts
*/
constructor(opts?: GoogleAuthOptions<T>) {
opts = opts || {};

constructor(opts: GoogleAuthOptions<T> = {}) {
this._cachedProjectId = opts.projectId || null;
this.cachedCredential = opts.authClient || null;
this.keyFilename = opts.keyFilename || opts.keyFile;
this.scopes = opts.scopes;
this.jsonContent = opts.credentials || null;
this.clientOptions = opts.clientOptions || {};
this.jsonContent = opts.credentials || null;
this.apiKey = opts.apiKey || this.clientOptions.apiKey || null;

// Cannot use both API Key + Credentials
if (this.apiKey && (this.jsonContent || this.clientOptions.credentials)) {
throw new RangeError(
GoogleAuthExceptionMessages.API_KEY_WITH_CREDENTIALS
);
}

if (opts.universeDomain) {
this.clientOptions.universeDomain = opts.universeDomain;
Expand Down Expand Up @@ -402,13 +419,10 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {
// This will also preserve one's configured quota project, in case they
// set one directly on the credential previously.
if (this.cachedCredential) {
return await this.prepareAndCacheADC(this.cachedCredential);
// cache, while preserving existing quota project preferences
return await this.#prepareAndCacheClient(this.cachedCredential, null);
}

// Since this is a 'new' ADC to cache we will use the environment variable
// if it's available. We prefer this value over the value from ADC.
const quotaProjectIdOverride = process.env['GOOGLE_CLOUD_QUOTA_PROJECT'];

let credential: JSONClient | null;
// Check for the existence of a local environment variable pointing to the
// location of the credential file. This is typically used in local
Expand All @@ -422,7 +436,7 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {
credential.scopes = this.getAnyScopes();
}

return await this.prepareAndCacheADC(credential, quotaProjectIdOverride);
return await this.#prepareAndCacheClient(credential);
}

// Look in the well-known credential file location.
Expand All @@ -434,7 +448,7 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {
} else if (credential instanceof BaseExternalAccountClient) {
credential.scopes = this.getAnyScopes();
}
return await this.prepareAndCacheADC(credential, quotaProjectIdOverride);
return await this.#prepareAndCacheClient(credential);
}

// Determine if we're running on GCE.
Expand All @@ -446,20 +460,15 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {
}

(options as ComputeOptions).scopes = this.getAnyScopes();
return await this.prepareAndCacheADC(
new Compute(options),
quotaProjectIdOverride
);
return await this.#prepareAndCacheClient(new Compute(options));
}

throw new Error(
'Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.'
);
throw new Error(GoogleAuthExceptionMessages.NO_ADC_FOUND);
}

private async prepareAndCacheADC(
credential: AnyAuthClient,
quotaProjectIdOverride?: string
async #prepareAndCacheClient(
credential: AnyAuthClient | T,
quotaProjectIdOverride = process.env['GOOGLE_CLOUD_QUOTA_PROJECT'] || null
): Promise<ADCResponse> {
const projectId = await this.getProjectIdOptional();

Expand Down Expand Up @@ -806,15 +815,14 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {

/**
* Create a credentials instance using the given API key string.
* The created client is not cached. In order to create and cache it use the {@link GoogleAuth.getClient `getClient`} method after first providing an {@link GoogleAuth.apiKey `apiKey`}.
*
* @param apiKey The API key string
* @param options An optional options object.
* @returns A JWT loaded from the key
*/
fromAPIKey(apiKey: string, options?: AuthClientOptions): JWT {
options = options || {};
const client = new JWT(options);
client.fromAPIKey(apiKey);
return client;
fromAPIKey(apiKey: string, options: AuthClientOptions = {}): JWT {
return new JWT({...options, apiKey});
}

/**
Expand Down Expand Up @@ -996,19 +1004,26 @@ export class GoogleAuth<T extends AuthClient = JSONClient> {
* provided configuration. If no options were passed, use Application
* Default Credentials.
*/
async getClient() {
if (!this.cachedCredential) {
if (this.jsonContent) {
this._cacheClientFromJSON(this.jsonContent, this.clientOptions);
} else if (this.keyFilename) {
const filePath = path.resolve(this.keyFilename);
const stream = fs.createReadStream(filePath);
await this.fromStreamAsync(stream, this.clientOptions);
} else {
await this.getApplicationDefaultAsync(this.clientOptions);
}
async getClient(): Promise<AnyAuthClient | T> {
if (this.cachedCredential) {
return this.cachedCredential;
} else if (this.jsonContent) {
return this._cacheClientFromJSON(this.jsonContent, this.clientOptions);
} else if (this.keyFilename) {
const filePath = path.resolve(this.keyFilename);
const stream = fs.createReadStream(filePath);
return await this.fromStreamAsync(stream, this.clientOptions);
} else if (this.apiKey) {
const client = await this.fromAPIKey(this.apiKey, this.clientOptions);
client.scopes = this.scopes;
const {credential} = await this.#prepareAndCacheClient(client);
return credential;
} else {
const {credential} = await this.getApplicationDefaultAsync(
this.clientOptions
);
return credential;
}
return this.cachedCredential!;
}

/**
Expand Down
2 changes: 0 additions & 2 deletions src/auth/oauth2client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,8 +528,6 @@ export class OAuth2Client extends AuthClient {
// TODO: refactor tests to make this private
_clientSecret?: string;

apiKey?: string;

refreshHandler?: GetRefreshHandlerCallback;

/**
Expand Down
Loading

0 comments on commit 5fc3bcc

Please sign in to comment.