diff --git a/.readme-partials.yaml b/.readme-partials.yaml index cce6a02f..7b26d0d2 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -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. diff --git a/README.md b/README.md index 55d86876..e15d28fc 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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) | diff --git a/samples/README.md b/samples/README.md index 115dba68..5ea1e11e 100644 --- a/samples/README.md +++ b/samples/README.md @@ -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) @@ -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). diff --git a/samples/authenticateAPIKey.js b/samples/authenticateAPIKey.js new file mode 100644 index 00000000..da9df2c1 --- /dev/null +++ b/samples/authenticateAPIKey.js @@ -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)); diff --git a/samples/package.json b/samples/package.json index 753884e2..be6ae7a1 100644 --- a/samples/package.json +++ b/samples/package.json @@ -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", diff --git a/src/auth/authclient.ts b/src/auth/authclient.ts index 69301db8..2f2b384c 100644 --- a/src/auth/authclient.ts +++ b/src/auth/authclient.ts @@ -72,6 +72,10 @@ interface AuthJSONOptions { */ export interface AuthClientOptions extends Partial> { + /** + * An API key to use, optional. + */ + apiKey?: string; credentials?: Credentials; /** @@ -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. @@ -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') ?? {}; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index b5cf27b1..06ae466b 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -85,6 +85,11 @@ export interface ADCResponse { } export interface GoogleAuthOptions { + /** + * An API key to use, optional. Cannot be used with {@link GoogleAuthOptions.credentials `credentials`}. + */ + apiKey?: string; + /** * An `AuthClient` to use */ @@ -102,6 +107,7 @@ export interface GoogleAuthOptions { /** * 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; @@ -136,7 +142,9 @@ export interface GoogleAuthOptions { 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' + @@ -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' + @@ -174,6 +184,7 @@ export class GoogleAuth { // To save the contents of the JSON credential file jsonContent: JWTInput | ExternalAccountClientOptions | null = null; + apiKey: string | null; cachedCredential: AnyAuthClient | T | null = null; @@ -202,15 +213,21 @@ export class GoogleAuth { * * @param opts */ - constructor(opts?: GoogleAuthOptions) { - opts = opts || {}; - + constructor(opts: GoogleAuthOptions = {}) { 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; @@ -402,13 +419,10 @@ export class GoogleAuth { // 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 @@ -422,7 +436,7 @@ export class GoogleAuth { credential.scopes = this.getAnyScopes(); } - return await this.prepareAndCacheADC(credential, quotaProjectIdOverride); + return await this.#prepareAndCacheClient(credential); } // Look in the well-known credential file location. @@ -434,7 +448,7 @@ export class GoogleAuth { } 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. @@ -446,20 +460,15 @@ export class GoogleAuth { } (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 { const projectId = await this.getProjectIdOptional(); @@ -806,15 +815,14 @@ export class GoogleAuth { /** * 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}); } /** @@ -996,19 +1004,26 @@ export class GoogleAuth { * 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 { + 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!; } /** diff --git a/src/auth/oauth2client.ts b/src/auth/oauth2client.ts index 4c60c5f9..fb813a7f 100644 --- a/src/auth/oauth2client.ts +++ b/src/auth/oauth2client.ts @@ -528,8 +528,6 @@ export class OAuth2Client extends AuthClient { // TODO: refactor tests to make this private _clientSecret?: string; - apiKey?: string; - refreshHandler?: GetRefreshHandlerCallback; /** diff --git a/test/test.googleauth.ts b/test/test.googleauth.ts index 3bb2ce95..afb56a3b 100644 --- a/test/test.googleauth.ts +++ b/test/test.googleauth.ts @@ -56,6 +56,7 @@ import {BaseExternalAccountClient} from '../src/auth/baseexternalclient'; import {AuthClient, DEFAULT_UNIVERSE} from '../src/auth/authclient'; import {ExternalAccountAuthorizedUserClient} from '../src/auth/externalAccountAuthorizedUserClient'; import {stringify} from 'querystring'; +import {GoogleAuthExceptionMessages} from '../src/auth/googleauth'; nock.disableNetConnect(); @@ -303,6 +304,30 @@ describe('googleauth', () => { assert.deepEqual(await auth.getRequestHeaders(''), customRequestHeaders); }); + it('should accept and use an `apiKey`', async () => { + const apiKey = 'myKey'; + const auth = new GoogleAuth({apiKey}); + const client = await auth.getClient(); + + assert.equal(client.apiKey, apiKey); + assert.deepEqual(await auth.getRequestHeaders(), { + 'X-Goog-Api-Key': apiKey, + }); + }); + + it('should not accept both an `apiKey` and `credentials`', async () => { + const apiKey = 'myKey'; + assert.throws( + () => + new GoogleAuth({ + credentials: {}, + // API key should supported via `clientOptions` + clientOptions: {apiKey}, + }), + new RangeError(GoogleAuthExceptionMessages.API_KEY_WITH_CREDENTIALS) + ); + }); + it('fromJSON should support the instantiated named export', () => { const result = auth.fromJSON(createJwtJSON()); assert(result); @@ -329,14 +354,6 @@ describe('googleauth', () => { assert.strictEqual(client.email, 'hello@youarecool.com'); }); - it('fromAPIKey should error given an invalid api key', () => { - assert.throws(() => { - // Test verifies invalid parameter tests, which requires cast to any. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (auth as any).fromAPIKey(null); - }); - }); - it('should make a request with the api key', async () => { const scope = nock(BASE_URL) .post(ENDPOINT) @@ -1423,12 +1440,14 @@ describe('googleauth', () => { }); it('should pass options to the JWT constructor via constructor', async () => { + const apiKey = 'my-api-key'; const subject = 'science!'; const auth = new GoogleAuth({ keyFilename: './test/fixtures/private.json', - clientOptions: {subject}, + clientOptions: {apiKey, subject}, }); const client = (await auth.getClient()) as JWT; + assert.strictEqual(client.apiKey, apiKey); assert.strictEqual(client.subject, subject); }); diff --git a/test/test.oauth2.ts b/test/test.oauth2.ts index ebd42ef0..7c4d3446 100644 --- a/test/test.oauth2.ts +++ b/test/test.oauth2.ts @@ -74,6 +74,13 @@ describe('oauth2', () => { sandbox.restore(); }); + it('should accept and set an `apiKey`', () => { + const API_KEY = 'TEST_API_KEY'; + const client = new OAuth2Client({apiKey: API_KEY}); + + assert.equal(client.apiKey, API_KEY); + }); + it('should generate a valid consent page url', done => { const opts = { access_type: ACCESS_TYPE,