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!: Request Revamp #1938

Merged
merged 11 commits into from
Feb 15, 2025
4 changes: 2 additions & 2 deletions .readme-partials.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ body: |-

## OAuth2

This library comes with an [OAuth2](https://developers.google.com/identity/protocols/OAuth2) client that allows you to retrieve an access token and refreshes the token and retry the request seamlessly if you also provide an `expiry_date` and the token is expired. The basics of Google's OAuth2 implementation is explained on [Google Authorization and Authentication documentation](https://developers.google.com/accounts/docs/OAuth2Login).
This library comes with an [OAuth2](https://developers.google.com/identity/protocols/OAuth2) client that allows you to retrieve an access token and refreshes the token and retry the request seamlessly if you also provide an `expiry_date` and the token is expired. The basics of Google's OAuth2 implementation is explained on [Google authorization and Authentication documentation](https://developers.google.com/accounts/docs/OAuth2Login).

In the following examples, you may need a `CLIENT_ID`, `CLIENT_SECRET` and `REDIRECT_URL`. You can find these pieces of information by going to the [Developer Console](https://console.cloud.google.com/), clicking your project > APIs & auth > credentials.

Expand Down Expand Up @@ -1185,7 +1185,7 @@ body: |-

// Get impersonated credentials:
const authHeaders = await targetClient.getRequestHeaders();
// Do something with `authHeaders.Authorization`.
// Do something with `authHeaders.get('authorization')`.

// Use impersonated credentials:
const url = 'https://www.googleapis.com/storage/v1/b?project=anotherProjectID'
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ main().catch(console.error);

## OAuth2

This library comes with an [OAuth2](https://developers.google.com/identity/protocols/OAuth2) client that allows you to retrieve an access token and refreshes the token and retry the request seamlessly if you also provide an `expiry_date` and the token is expired. The basics of Google's OAuth2 implementation is explained on [Google Authorization and Authentication documentation](https://developers.google.com/accounts/docs/OAuth2Login).
This library comes with an [OAuth2](https://developers.google.com/identity/protocols/OAuth2) client that allows you to retrieve an access token and refreshes the token and retry the request seamlessly if you also provide an `expiry_date` and the token is expired. The basics of Google's OAuth2 implementation is explained on [Google authorization and Authentication documentation](https://developers.google.com/accounts/docs/OAuth2Login).

In the following examples, you may need a `CLIENT_ID`, `CLIENT_SECRET` and `REDIRECT_URL`. You can find these pieces of information by going to the [Developer Console](https://console.cloud.google.com/), clicking your project > APIs & auth > credentials.

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

// Get impersonated credentials:
const authHeaders = await targetClient.getRequestHeaders();
// Do something with `authHeaders.Authorization`.
// Do something with `authHeaders.get('authorization')`.

// Use impersonated credentials:
const url = 'https://www.googleapis.com/storage/v1/b?project=anotherProjectID'
Expand Down
4 changes: 2 additions & 2 deletions browser-test/test.oauth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ const FEDERATED_SIGNON_JWK_CERTS = [
},
];
const FEDERATED_SIGNON_JWK_CERTS_AXIOS_RESPONSE = {
headers: {
headers: new Headers({
'cache-control':
'cache-control: public, max-age=24000, must-revalidate, no-transform',
},
}),
data: {keys: FEDERATED_SIGNON_JWK_CERTS},
};

Expand Down
11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"gaxios": "^7.0.0-rc.4",
"gcp-metadata": "^7.0.0-rc.1",
"gtoken": "^8.0.0-rc.1",
"jws": "^4.0.0"
},
"devDependencies": {
Expand Down Expand Up @@ -54,7 +54,7 @@
"mocha": "^9.2.2",
"mv": "^2.1.1",
"ncp": "^2.0.0",
"nock": "^13.0.0",
"nock": "^14.0.1",
"null-loader": "^4.0.0",
"puppeteer": "^24.0.0",
"sinon": "^18.0.0",
Expand Down Expand Up @@ -84,8 +84,7 @@
"browser-test": "karma start",
"docs-test": "linkinator docs",
"predocs-test": "npm run docs",
"prelint": "cd samples; npm link ../; npm install",
"precompile": "gts clean"
"prelint": "cd samples; npm link ../; npm install"
},
"license": "Apache-2.0"
}
2 changes: 1 addition & 1 deletion samples/idTokenFromMetadataServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

/**
* Uses the Google Cloud metadata server environment to create an identity token
* and add it to the HTTP request as part of an Authorization header.
* and add it to the HTTP request as part of an authorization header.
*
* @param {string} targetAudience - The url or target audience to obtain the ID token for.
*/
Expand Down
4 changes: 2 additions & 2 deletions samples/test/externalclient.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,15 +356,15 @@ describe('samples for external-account', () => {
if (req.url === '/token' && req.method === 'GET') {
// Confirm expected header is passed along the request.
if (req.headers['my-header'] === 'some-value') {
res.setHeader('Content-Type', 'application/json');
res.setHeader('content-type', 'application/json');
res.writeHead(200);
res.end(
JSON.stringify({
access_token: oidcToken,
})
);
} else {
res.setHeader('Content-Type', 'application/json');
res.setHeader('content-type', 'application/json');
res.writeHead(400);
res.end(
JSON.stringify({
Expand Down
63 changes: 44 additions & 19 deletions src/auth/authclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,10 @@ export interface CredentialsClient {
* resolves with authorization header fields.
*
* The result has the form:
* { Authorization: 'Bearer <access_token_value>' }
* { authorization: 'Bearer <access_token_value>' }
* @param url The URI being authorized.
*/
getRequestHeaders(url?: string): Promise<Headers>;
getRequestHeaders(url?: string | URL): Promise<Headers>;

/**
* Provides an alternative Gaxios request implementation with auth credentials
Expand Down Expand Up @@ -251,10 +251,13 @@ export abstract class AuthClient
* resolves with authorization header fields.
*
* The result has the form:
* { Authorization: 'Bearer <access_token_value>' }
* ```ts
* new Headers({'authorization': 'Bearer <access_token_value>'});
* ```
*
* @param url The URI being authorized.
*/
abstract getRequestHeaders(url?: string): Promise<Headers>;
abstract getRequestHeaders(url?: string | URL): Promise<Headers>;

/**
* @return A promise that resolves with the current GCP access token
Expand Down Expand Up @@ -285,35 +288,58 @@ export abstract class AuthClient
// the x-goog-user-project header, to indicate an alternate account for
// billing and quota:
if (
!headers['x-goog-user-project'] && // don't override a value the user sets.
!headers.has('x-goog-user-project') && // don't override a value the user sets.
this.quotaProjectId
) {
headers['x-goog-user-project'] = this.quotaProjectId;
headers.set('x-goog-user-project', this.quotaProjectId);
}
return headers;
}

/**
* Adds the `x-goog-user-project` and `authorization` headers to the target Headers
* object, if they exist on the source.
*
* @param target the headers to target
* @param source the headers to source from
* @returns the target headers
*/
protected addUserProjectAndAuthHeaders<T extends Headers>(
target: T,
source: Headers
): T {
const xGoogUserProject = source.get('x-goog-user-project');
const authorizationHeader = source.get('authorization');

if (xGoogUserProject) {
target.set('x-goog-user-project', xGoogUserProject);
}

if (authorizationHeader) {
target.set('authorization', authorizationHeader);
}

return target;
}

static readonly DEFAULT_REQUEST_INTERCEPTOR: Parameters<
Gaxios['interceptors']['request']['add']
>[0] = {
resolved: async config => {
const headers = config.headers || {};

// Set `x-goog-api-client`, if not already set
if (!headers['x-goog-api-client']) {
if (!config.headers.has('x-goog-api-client')) {
const nodeVersion = process.version.replace(/^v/, '');
headers['x-goog-api-client'] = `gl-node/${nodeVersion}`;
config.headers.set('x-goog-api-client', `gl-node/${nodeVersion}`);
}

// Set `User-Agent`
if (!headers['User-Agent']) {
headers['User-Agent'] = USER_AGENT;
} else if (!headers['User-Agent'].includes(`${PRODUCT_NAME}/`)) {
headers['User-Agent'] = `${headers['User-Agent']} ${USER_AGENT}`;
const userAgent = config.headers.get('User-Agent');
if (!userAgent) {
config.headers.set('User-Agent', USER_AGENT);
} else if (!userAgent.includes(`${PRODUCT_NAME}/`)) {
config.headers.set('User-Agent', `${userAgent} ${USER_AGENT}`);
}

config.headers = headers;

return config;
},
};
Expand All @@ -337,9 +363,8 @@ export abstract class AuthClient
}
}

export interface Headers {
[index: string]: string;
}
// TypeScript does not have `HeadersInit` in the standard types yet
export type HeadersInit = ConstructorParameters<typeof Headers>[0];

export interface GetAccessTokenResponse {
token?: string | null;
Expand Down
16 changes: 8 additions & 8 deletions src/auth/awsclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {

import {DefaultAwsSecurityCredentialsSupplier} from './defaultawssecuritycredentialssupplier';
import {originalOrCamelOptions, SnakeToCamelObject} from '../util';
import {Gaxios} from 'gaxios';

/**
* AWS credentials JSON interface. This is used for AWS workloads.
Expand Down Expand Up @@ -230,7 +231,7 @@ export class AwsClient extends BaseExternalAccountClient {
// The GCP STS endpoint expects the headers to be formatted as:
// [
// {key: 'x-amz-date', value: '...'},
// {key: 'Authorization', value: '...'},
// {key: 'authorization', value: '...'},
// ...
// ]
// And then serialized as:
Expand All @@ -240,7 +241,7 @@ export class AwsClient extends BaseExternalAccountClient {
// headers: [{key: 'x-amz-date', value: '...'}, ...]
// }))
const reformattedHeader: {key: string; value: string}[] = [];
const extendedHeaders = Object.assign(
const extendedHeaders = Gaxios.mergeHeaders(
{
// The full, canonical resource name of the workload identity pool
// provider, with or without the HTTPS prefix.
Expand All @@ -250,13 +251,12 @@ export class AwsClient extends BaseExternalAccountClient {
},
options.headers
);

// Reformat header to GCP STS expected format.
for (const key in extendedHeaders) {
reformattedHeader.push({
key,
value: extendedHeaders[key],
});
}
extendedHeaders.forEach((value, key) =>
reformattedHeader.push({key, value})
);

// Serialize the reformatted signed request.
return encodeURIComponent(
JSON.stringify({
Expand Down
Loading
Loading