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: Extend API Key Support #1835

Merged
merged 29 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c67e15b
feat: Extend API Key Support
d-goog Jul 11, 2024
cfcc2a2
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Jul 11, 2024
0b1710e
feat: Support `apiKey` as an ADC fallback
d-goog Jul 11, 2024
e54f54a
Merge branch 'extend-apikey-support' of github.com:googleapis/google-…
d-goog Jul 11, 2024
ae834cb
Merge branch 'main' of github.com:googleapis/google-auth-library-node…
d-goog Jul 11, 2024
d6c51f9
refactor: Move `apiKey` to base client options
d-goog Aug 2, 2024
7a5c54c
docs: clarity
d-goog Aug 2, 2024
247f384
refactor: API Key Support
d-goog Aug 2, 2024
81a10b9
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Aug 2, 2024
70b7eb6
fix: type
d-goog Aug 2, 2024
ecc40ef
Merge branch 'extend-apikey-support' of github.com:googleapis/google-…
d-goog Aug 2, 2024
65c69a7
feat: Export Error Messages
d-goog Aug 2, 2024
d4bf043
test: Add tests for API Key Support
d-goog Aug 2, 2024
db65b50
Merge branch 'main' of github.com:googleapis/google-auth-library-node…
d-goog Aug 2, 2024
66829d8
test: cleanup
d-goog Aug 2, 2024
5f47a63
docs: Clarifications
d-goog Aug 2, 2024
12ae76c
refactor: streamline
d-goog Aug 6, 2024
f5779bf
Merge branch 'main' of github.com:googleapis/google-auth-library-node…
d-goog Aug 14, 2024
0bba085
chore: merge cleanup
d-goog Aug 14, 2024
5f9eaef
Merge branch 'main' into extend-apikey-support
d-goog Aug 16, 2024
4191d61
docs(sample): Add API Key Sample
d-goog Aug 16, 2024
70ab441
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Aug 16, 2024
e4d3e5d
chore: OCD
d-goog Aug 16, 2024
1bd5a38
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Aug 16, 2024
b0ce42f
Apply suggestions from code review
d-goog Aug 19, 2024
e6dfc6d
Merge branch 'main' into extend-apikey-support
d-goog Aug 19, 2024
4eba6d2
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Aug 19, 2024
b7d6a91
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Aug 19, 2024
16521ce
Merge branch 'extend-apikey-support' of https://github.com/googleapis…
gcf-owl-bot[bot] Aug 19, 2024
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
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'
}
})
```

danielbankhead marked this conversation as resolved.
Show resolved Hide resolved
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`.
danielbankhead marked this conversation as resolved.
Show resolved Hide resolved

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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

//
// 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
Loading