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: add methods for fetching and using id tokens (second implementation) #867

Merged
merged 23 commits into from
Jan 14, 2020
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,51 @@ async function main() {
main().catch(console.error);
```

## Working with ID Tokens
If your application is running behind Cloud Run, or using Cloud Identity-Aware
Proxy (IAP), you will need to fetch an ID token to access your application. For
this, use the method `getIdTokenClient` on the `GoogleAuth` client.

For invoking Cloud Run services, your service account will need the
[`Cloud Run Invoker`](https://cloud.google.com/run/docs/authenticating/service-to-service)
IAM permission.

``` js
// Make a request to a protected Cloud Run
const {GoogleAuth} = require('google-auth-library');

async function main() {
const url = 'https://cloud-run-url.com';
const auth = new GoogleAuth();
const client = auth.getIdTokenClient(url);
const res = await client.request({url});
console.log(res.data);
}

main().catch(console.error);
```

For invoking Cloud Identity-Aware Proxy, you will need to pass the Client ID
used when you set up your protected resource as the target audience.

``` js
// Make a request to a protected Cloud Identity-Aware Proxy (IAP) resource
const {GoogleAuth} = require('google-auth-library');

async function main()
const targetAudience = 'iap-client-id';
const url = 'https://iap-url.com';
const auth = new GoogleAuth();
const client = auth.getIdTokenClient(targetAudience);
const res = await client.request({url});
console.log(res.data);
}

main().catch(console.error);
```

See how to [secure your IAP app with signed headers](https://cloud.google.com/iap/docs/signed-headers-howto).

## Questions/problems?

* Ask your development related questions on [Stack Overflow][stackoverflow].
Expand Down
41 changes: 0 additions & 41 deletions samples/iap.js

This file was deleted.

40 changes: 40 additions & 0 deletions samples/idtokens-cloudrun.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2020 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.

'use strict';

Copy link
Contributor

Choose a reason for hiding this comment

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

For new samples, I guess we're adding a metadata block to the top of each sample file, like this:

// sample-metadata:
//   title: Create Topic
//   description: Creates a new topic.
//   usage: node createTopic.js <topic-name>

That might be a good addition here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added! PTAL

async function main(
url = 'https://service-1234-uc.a.run.app'
bshaffer marked this conversation as resolved.
Show resolved Hide resolved
) {
// [START google_auth_idtoken_cloudrun]
/**
* TODO(developer): Uncomment these variables before running the sample.
*/
// const url = 'https://service-1234-uc.a.run.app';

const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();

async function request() {
console.info(`request Cloud Run ${url}`);
const client = await auth.getIdTokenClient(url);
const res = await client.request({url});
console.info(res.data);
}

request();
// [END google_auth_idtoken_cloudrun]
}

const args = process.argv.slice(2);
main(...args).catch(console.error);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the problem described in this issue may apply here:

googleapis/nodejs-asset#242

42 changes: 42 additions & 0 deletions samples/idtokens-iap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2020 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.

'use strict';

bshaffer marked this conversation as resolved.
Show resolved Hide resolved
async function main(
url = 'https://some.iap.url',
targetAudience = 'IAP_CLIENT_ID.apps.googleusercontent.com'
) {
// [START google_auth_idtoken_iap]
/**
* TODO(developer): Uncomment these variables before running the sample.
*/
// const url = 'https://some.iap.url';
// const targetAudience = 'IAP_CLIENT_ID.apps.googleusercontent.com';

const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();

async function request() {
console.info(`request IAP ${url} with target audience ${targetAudience}`);
const client = await auth.getIdTokenClient(targetAudience);
const res = await client.request({url});
console.info(res.data);
}

request();
// [END google_auth_idtoken_iap]
}

const args = process.argv.slice(2);
main(...args).catch(console.error);
9 changes: 9 additions & 0 deletions samples/test/jwt.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,13 @@ describe('samples', () => {
assert.match(output, /Headers:/);
assert.match(output, /DNS Info:/);
});

it('should fetch ID token based on target audience', async () => {
// process.env.CLOUD_RUN_URL should be a cloud run container, protected with
// IAP, running gcr.io/cloudrun/hello:
const url =
process.env.CLOUD_RUN_URL || 'https://hello-rftcw63abq-uc.a.run.app';
const output = execSync(`node idtokens ${url} ${url}`);
assert.match(output, /What's next?/);
});
});
23 changes: 23 additions & 0 deletions src/auth/computeclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import * as gcpMetadata from 'gcp-metadata';
import * as messages from '../messages';

import {CredentialRequest, Credentials} from './credentials';
import {IdTokenProvider} from './idtokenclient';
import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client';

export interface ComputeOptions extends RefreshOptions {
Expand Down Expand Up @@ -101,6 +102,28 @@ export class Compute extends OAuth2Client {
return {tokens, res: null};
}

/**
* Fetches an ID token.
* @param targetAudience the audience for the fetched ID token.
*/
async fetchIdToken(targetAudience: string): Promise<string> {
const idTokenPath =
`service-accounts/${this.serviceAccountEmail}/identity` +
`?audience=${targetAudience}`;
let idToken: string;
try {
const instanceOptions: gcpMetadata.Options = {
property: idTokenPath,
};
idToken = await gcpMetadata.instance(instanceOptions);
} catch (e) {
e.message = `Could not fetch ID token: ${e.message}`;
throw e;
}

return idToken;
}

protected wrapError(e: GaxiosError) {
const res = e.response;
if (res && res.status) {
Expand Down
16 changes: 16 additions & 0 deletions src/auth/googleauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {DefaultTransporter, Transporter} from '../transporters';

import {Compute, ComputeOptions} from './computeclient';
import {CredentialBody, JWTInput} from './credentials';
import {IdTokenClient, IdTokenProvider} from './idtokenclient';
import {GCPEnv, getEnv} from './envDetect';
import {JWT, JWTOptions} from './jwtclient';
import {
Expand Down Expand Up @@ -728,6 +729,21 @@ export class GoogleAuth {
return this.cachedCredential!;
}

/**
* Creates a client which will fetch an ID token for authorization.
* @param targetAudience the audience for the fetched ID token.
* @returns IdTokenClient for making HTTP calls authenticated with ID tokens.
*/
async getIdTokenClient(targetAudience: string): Promise<IdTokenClient> {
const client = await this.getClient();
if (!('fetchIdToken' in client)) {
throw new Error(
'Cannot fetch ID token in this environment, use GCE or set the GOOGLE_APPLICATION_CREDENTIALS environment variable to a service account credentials JSON file.'
);
}
return new IdTokenClient({targetAudience, idTokenProvider: client});
}

/**
* Automatically obtain application default credentials, and return
* an access token for making requests.
Expand Down
101 changes: 101 additions & 0 deletions src/auth/idtokenclient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright 2020 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.

import {
GaxiosError,
GaxiosOptions,
GaxiosPromise,
GaxiosResponse,
} from 'gaxios';

import {BodyResponseCallback} from '../transporters';

import {AuthClient} from './authclient';
import {OAuth2Client} from './oauth2client';

export interface IdTokenOptions {
/**
* The client to make the request to fetch an ID token.
*/
idTokenProvider: IdTokenProvider;
/**
* The audience to use when requesting an ID token.
*/
targetAudience: string;
}

export interface IdTokenProvider {
fetchIdToken: (targetAudience: string) => Promise<string>;
}

export class IdTokenClient extends AuthClient {
bshaffer marked this conversation as resolved.
Show resolved Hide resolved
targetAudience: string;
idTokenProvider: IdTokenProvider;

/**
* Google ID Token client
*
* Retrieve access token from the metadata server.
* See: https://developers.google.com/compute/docs/authentication
*/
constructor(options: IdTokenOptions) {
super();
this.targetAudience = options.targetAudience;
this.idTokenProvider = options.idTokenProvider;
}

/**
* Provides a request implementation with OAuth 2.0 flow. If credentials have
* a refresh_token, in cases of HTTP 401 and 403 responses, it automatically
* asks for a new access token and replays the unsuccessful request.
* @param opts Request options.
* @param callback callback.
* @return Request object
*/
request<T>(opts: GaxiosOptions): GaxiosPromise<T>;
request<T>(opts: GaxiosOptions, callback: BodyResponseCallback<T>): void;
request<T>(
opts: GaxiosOptions,
callback?: BodyResponseCallback<T>
): GaxiosPromise<T> | void {
if (callback) {
this.requestAsync<T>(opts).then(
r => callback(null, r),
e => {
return callback(e, e.response);
}
);
} else {
return this.requestAsync<T>(opts);
}
}

protected async requestAsync<T>(
opts: GaxiosOptions,
retry = false
): Promise<GaxiosResponse<T>> {
let r2: GaxiosResponse;

const idToken = await this.idTokenProvider.fetchIdToken(
this.targetAudience
);

opts.headers = opts.headers || {};
opts.headers.Authorization = `Bearer ${idToken}`;

r2 = await this.transporter.request<T>(opts);

return r2;
}
}