Skip to content

Commit

Permalink
feat: support for verifying ES256 and retrieving IAP public keys (#887)
Browse files Browse the repository at this point in the history
  • Loading branch information
bshaffer committed Feb 12, 2020
1 parent e671629 commit a98e386
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 10 deletions.
32 changes: 31 additions & 1 deletion README.md
Expand Up @@ -335,6 +335,7 @@ main().catch(console.error);
```

## Working with ID Tokens
### Fetching 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.
Expand All @@ -358,6 +359,8 @@ async function main() {
main().catch(console.error);
```

A complete example can be found in [`samples/idtokens-cloudrun.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-cloudrun.js).

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.

Expand All @@ -377,7 +380,34 @@ async function main()
main().catch(console.error);
```

See how to [secure your IAP app with signed headers](https://cloud.google.com/iap/docs/signed-headers-howto).
A complete example can be found in [`samples/idtokens-iap.js`](https://github.com/googleapis/google-auth-library-nodejs/blob/master/samples/idtokens-iap.js).

### Verifying ID Tokens

If you've [secured your IAP app with signed headers](https://cloud.google.com/iap/docs/signed-headers-howto),
you can use this library to verify the IAP header:

```js
const {OAuth2Client} = require('google-auth-library');
// Expected audience for App Engine.
const expectedAudience = `/projects/your-project-number/apps/your-project-id`;
// IAP issuer
const issuers = ['https://cloud.google.com/iap'];
// Verify the token. OAuth2Client throws an Error if verification fails
const oAuth2Client = new OAuth2Client();
const response = await oAuth2Client.getIapCerts();
const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync(
idToken,
response.pubkeys,
expectedAudience,
issuers
);

// Print out the info contained in the IAP ID token
console.log(ticket)
```

A complete example can be found in `samples/verifyIdToken-iap.js`.

## Questions/problems?

Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -19,6 +19,7 @@
"dependencies": {
"arrify": "^2.0.0",
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"fast-text-encoding": "^1.0.0",
"gaxios": "^2.1.0",
"gcp-metadata": "^3.3.0",
Expand Down
62 changes: 62 additions & 0 deletions samples/verifyIdToken-iap.js
@@ -0,0 +1,62 @@
// 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';

// [START iap_validate_jwt]
const {OAuth2Client} = require('google-auth-library');

/**
* Verify the ID token from IAP
* @see https://cloud.google.com/iap/docs/signed-headers-howto
*/
async function main(
iapJwt,
projectNumber = '',
projectId = '',
backendServiceId = ''
) {
// set Audience
let expectedAudience = null;
if (projectNumber && projectId) {
// Expected Audience for App Engine.
expectedAudience = `/projects/${projectNumber}/apps/${projectId}`;
} else if (projectNumber && backendServiceId) {
// Expected Audience for Compute Engine
expectedAudience = `/projects/${projectNumber}/global/backendServices/${backendServiceId}`;
}

const oAuth2Client = new OAuth2Client();

// Verify the id_token, and access the claims.
const response = await oAuth2Client.getIapPublicKeys();
const ticket = await oAuth2Client.verifySignedJwtWithCertsAsync(
iapJwt,
response.pubkeys,
expectedAudience,
['https://cloud.google.com/iap']
);

// Print out the info contained in the IAP ID token
console.log(ticket);

if (!expectedAudience) {
console.log(
'Audience not verified! Supply a projectNumber and projectID to verify'
);
}
}
// [END iap_validate_jwt]

const args = process.argv.slice(2);
main(...args).catch(console.error);
71 changes: 68 additions & 3 deletions src/auth/oauth2client.ts
Expand Up @@ -20,6 +20,7 @@ import {
} from 'gaxios';
import * as querystring from 'querystring';
import * as stream from 'stream';
import * as formatEcdsa from 'ecdsa-sig-formatter';

import {createCrypto, JwkCertificate, hasBrowserCrypto} from '../crypto/crypto';
import * as messages from '../messages';
Expand All @@ -28,7 +29,6 @@ import {BodyResponseCallback} from '../transporters';
import {AuthClient} from './authclient';
import {CredentialRequest, Credentials, JWTInput} from './credentials';
import {LoginTicket, TokenPayload} from './loginticket';

/**
* The results from the `generateCodeVerifierAsync` method. To learn more,
* See the sample:
Expand All @@ -51,6 +51,10 @@ export interface Certificates {
[index: string]: string | JwkCertificate;
}

export interface PublicKeys {
[index: string]: string;
}

export interface Headers {
[index: string]: string;
}
Expand Down Expand Up @@ -349,6 +353,19 @@ export interface FederatedSignonCertsResponse {
res?: GaxiosResponse<void> | null;
}

export interface GetIapPublicKeysCallback {
(
err: GaxiosError | null,
pubkeys?: PublicKeys,
response?: GaxiosResponse<void> | null
): void;
}

export interface IapPublicKeysResponse {
pubkeys: PublicKeys;
res?: GaxiosResponse<void> | null;
}

export interface RevokeCredentialsResult {
success: boolean;
}
Expand Down Expand Up @@ -462,6 +479,12 @@ export class OAuth2Client extends AuthClient {
private static readonly GOOGLE_OAUTH2_FEDERATED_SIGNON_JWK_CERTS_URL_ =
'https://www.googleapis.com/oauth2/v3/certs';

/**
* Google Sign on certificates in JWK format.
*/
private static readonly GOOGLE_OAUTH2_IAP_PUBLIC_KEY_URL_ =
'https://www.gstatic.com/iap/verify/public_key';

/**
* Clock skew - five minutes in seconds
*/
Expand Down Expand Up @@ -1108,6 +1131,43 @@ export class OAuth2Client extends AuthClient {
return {certs: certificates, format, res};
}

/**
* Gets federated sign-on certificates to use for verifying identity tokens.
* Returns certs as array structure, where keys are key ids, and values
* are certificates in either PEM or JWK format.
* @param callback Callback supplying the certificates
*/
getIapPublicKeys(): Promise<IapPublicKeysResponse>;
getIapPublicKeys(callback: GetIapPublicKeysCallback): void;
getIapPublicKeys(
callback?: GetIapPublicKeysCallback
): Promise<IapPublicKeysResponse> | void {
if (callback) {
this.getIapPublicKeysAsync().then(
r => callback(null, r.pubkeys, r.res),
callback
);
} else {
return this.getIapPublicKeysAsync();
}
}

async getIapPublicKeysAsync(): Promise<IapPublicKeysResponse> {
const nowTime = new Date().getTime();

let res: GaxiosResponse;
const url: string = OAuth2Client.GOOGLE_OAUTH2_IAP_PUBLIC_KEY_URL_;

try {
res = await this.transporter.request({url});
} catch (e) {
e.message = `Failed to retrieve verification certificates: ${e.message}`;
throw e;
}

return {pubkeys: res.data, res};
}

verifySignedJwtWithCerts() {
// To make the code compatible with browser SubtleCrypto we need to make
// this method async.
Expand All @@ -1128,7 +1188,7 @@ export class OAuth2Client extends AuthClient {
*/
async verifySignedJwtWithCertsAsync(
jwt: string,
certs: Certificates,
certs: Certificates | PublicKeys,
requiredAudience: string | string[],
issuers?: string[],
maxExpiry?: number
Expand All @@ -1144,7 +1204,7 @@ export class OAuth2Client extends AuthClient {
throw new Error('Wrong number of segments in token: ' + jwt);
}
const signed = segments[0] + '.' + segments[1];
const signature = segments[2];
let signature = segments[2];

let envelope;
let payload: TokenPayload;
Expand Down Expand Up @@ -1177,6 +1237,11 @@ export class OAuth2Client extends AuthClient {
}

const cert = certs[envelope.kid];

if (envelope.alg === 'ES256') {
signature = formatEcdsa.joseToDer(signature, 'ES256').toString('base64');
}

const verified = await crypto.verify(cert, signed, signature);

if (!verified) {
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/ecdsapublickeys.json
@@ -0,0 +1,4 @@
{
"2nMJtw": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9e1x7YRZg53A5zIJ0p2ZQ9yTrgPL\nGIf4ntOk+4O2R2+ryIObueyenPXE92tYG1NlKjDNyJLc7tsxi0UUnyxpig==\n-----END PUBLIC KEY-----\n",
"f9R3yg": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESqCmEwytkqG6tL6a2GTQGmSNI4jH\nYo5MeDUs7DpETVhCXXLIFrLg2sZvNqw8SGnnonLoeqgOSqRdjJBGt4I6jQ==\n-----END PUBLIC KEY-----\n"
}
18 changes: 18 additions & 0 deletions test/fixtures/fake-ecdsa-private.pem
@@ -0,0 +1,18 @@
-----BEGIN EC PARAMETERS-----
MIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP//////////
/////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6
k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3gZ9+
kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO5+tK
fA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racXnoTz
ucrC/GMlUQIBAQ==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIIBaAIBAQQgI59iRg8vYTW9VySmCM3Tvn+WPLKQr132U5HGec3HTgWggfowgfcC
AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////
MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr
vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE
axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W
K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8
YyVRAgEBoUQDQgAEUF5XFvBU1J2PP2Ggh/DiLNv9l4MTM/edN145vGOZIvWe4QBp
FaqzLN7WjTP7BiJCXI044iqRbuDGc2goPf8LMw==
-----END EC PRIVATE KEY-----
9 changes: 9 additions & 0 deletions test/fixtures/fake-ecdsa-public.pem
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAA
AAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA////
///////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSd
NgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5
RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA
//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABFBeVxbwVNSdjz9hoIfw4izb
/ZeDEzP3nTdeObxjmSL1nuEAaRWqsyze1o0z+wYiQlyNOOIqkW7gxnNoKD3/CzM=
-----END PUBLIC KEY-----
10 changes: 5 additions & 5 deletions test/test.jwt.ts
Expand Up @@ -195,7 +195,7 @@ describe('jwt', () => {
});

it('gets a jwt header access token', async () => {
const keys = keypair(1024 /* bitsize of private key */);
const keys = keypair(512 /* bitsize of private key */);
const email = 'foo@serviceaccount.com';
const jwt = new JWT({
email: 'foo@serviceaccount.com',
Expand All @@ -216,7 +216,7 @@ describe('jwt', () => {
});

it('gets a jwt header access token with key id', async () => {
const keys = keypair(1024 /* bitsize of private key */);
const keys = keypair(512 /* bitsize of private key */);
const jwt = new JWT({
email: 'foo@serviceaccount.com',
key: keys.private,
Expand All @@ -236,7 +236,7 @@ describe('jwt', () => {
});

it('should accept additionalClaims', async () => {
const keys = keypair(1024 /* bitsize of private key */);
const keys = keypair(512 /* bitsize of private key */);
const someClaim = 'cat-on-my-desk';
const jwt = new JWT({
email: 'foo@serviceaccount.com',
Expand All @@ -256,7 +256,7 @@ describe('jwt', () => {
});

it('should accept additionalClaims that include a target_audience', async () => {
const keys = keypair(1024 /* bitsize of private key */);
const keys = keypair(512 /* bitsize of private key */);
const jwt = new JWT({
email: 'foo@serviceaccount.com',
key: keys.private,
Expand Down Expand Up @@ -821,7 +821,7 @@ describe('jwt', () => {
credentials: Object.assign(
require('../../test/fixtures/service-account-with-quota.json'),
{
private_key: keypair(1024 /* bitsize of private key */).private,
private_key: keypair(512 /* bitsize of private key */).private,
}
),
});
Expand Down
2 changes: 1 addition & 1 deletion test/test.jwtaccess.ts
Expand Up @@ -33,7 +33,7 @@ describe('jwtaccess', () => {
type: 'service_account',
};

const keys = keypair(1024 /* bitsize of private key */);
const keys = keypair(512 /* bitsize of private key */);
const testUri = 'http:/example.com/my_test_service';
const email = 'foo@serviceaccount.com';

Expand Down

0 comments on commit a98e386

Please sign in to comment.