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: default self-signed JWTs #1054

Merged
merged 12 commits into from
Sep 22, 2020
12 changes: 11 additions & 1 deletion src/auth/googleauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ export interface GoogleAuthOptions {
*/
clientOptions?: JWTOptions | OAuth2ClientOptions | UserRefreshClientOptions;

/**
* Scopes populated by the client library by default. We differentiate between
* these and user defined scopes when deciding whether to use a self-signed JWT.
*/
defaultScopes?: string | string[];
bcoe marked this conversation as resolved.
Show resolved Hide resolved

/**
* Required scopes for the desired API request
*/
Expand Down Expand Up @@ -121,6 +127,7 @@ export class GoogleAuth {

private keyFilename?: string;
private scopes?: string | string[];
private defaultScopes?: string | string[];
private clientOptions?: RefreshOptions;

/**
Expand All @@ -132,6 +139,7 @@ export class GoogleAuth {
opts = opts || {};
this._cachedProjectId = opts.projectId || null;
this.keyFilename = opts.keyFilename || opts.keyFile;
this.defaultScopes = opts.defaultScopes;
this.scopes = opts.scopes;
this.jsonContent = opts.credentials || null;
this.clientOptions = opts.clientOptions;
Expand Down Expand Up @@ -244,6 +252,7 @@ export class GoogleAuth {
);
if (credential) {
if (credential instanceof JWT) {
credential.defaultScopes = this.scopes;
credential.scopes = this.scopes;
}
this.cachedCredential = credential;
Expand Down Expand Up @@ -282,7 +291,7 @@ export class GoogleAuth {

// For GCE, just return a default ComputeClient. It will take care of
// the rest.
(options as ComputeOptions).scopes = this.scopes;
(options as ComputeOptions).scopes = this.scopes || this.defaultScopes;
this.cachedCredential = new Compute(options);
projectId = await this.getProjectId();
return {projectId, credential: this.cachedCredential};
Expand Down Expand Up @@ -420,6 +429,7 @@ export class GoogleAuth {
if (json.type === 'authorized_user') {
client = new UserRefreshClient(options);
} else {
(options as JWTOptions).defaultScopes = this.defaultScopes;
(options as JWTOptions).scopes = this.scopes;
client = new JWT(options);
}
Expand Down
10 changes: 6 additions & 4 deletions src/auth/jwtclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface JWTOptions extends RefreshOptions {
keyFile?: string;
key?: string;
keyId?: string;
defaultScopes?: string | string[];
scopes?: string | string[];
subject?: string;
additionalClaims?: {};
Expand All @@ -40,6 +41,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider {
keyFile?: string;
key?: string;
keyId?: string;
defaultScopes?: string | string[];
scopes?: string | string[];
scope?: string;
subject?: string;
Expand Down Expand Up @@ -120,7 +122,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider {
protected async getRequestMetadataAsync(
url?: string | null
): Promise<RequestMetadataResponse> {
if (!this.apiKey && !this.hasScopes() && url) {
if (!this.apiKey && !this.hasUserScopes() && url) {
if (
this.additionalClaims &&
(this.additionalClaims as {
Expand Down Expand Up @@ -159,7 +161,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider {
const gtoken = new GoogleToken({
iss: this.email,
sub: this.subject,
scope: this.scopes,
scope: this.scopes || this.defaultScopes,
keyFile: this.keyFile,
key: this.key,
additionalClaims: {target_audience: targetAudience},
Expand All @@ -176,7 +178,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider {
/**
* Determine if there are currently scopes available.
*/
private hasScopes() {
private hasUserScopes() {
if (!this.scopes) {
return false;
}
Expand Down Expand Up @@ -248,7 +250,7 @@ export class JWT extends OAuth2Client implements IdTokenProvider {
this.gtoken = new GoogleToken({
iss: this.email,
sub: this.subject,
scope: this.scopes,
scope: this.scopes || this.defaultScopes,
keyFile: this.keyFile,
key: this.key,
additionalClaims: this.additionalClaims,
Expand Down
57 changes: 57 additions & 0 deletions test/test.jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import * as sinon from 'sinon';

import {GoogleAuth, JWT} from '../src';
import {CredentialRequest, JWTInput} from '../src/auth/credentials';
import * as jwtaccess from '../src/auth/jwtaccess';

describe('jwt', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
Expand Down Expand Up @@ -783,4 +784,60 @@ describe('jwt', () => {
}
assert.fail('failed to throw');
});

describe('self-signed JWT', () => {
const sandbox = sinon.createSandbox();
let stubJWTAccess: sinon.SinonStub;
beforeEach(() => {
stubJWTAccess = sandbox.stub(jwtaccess, 'JWTAccess').returns({
getRequestHeaders: sinon.stub().returns({}),
});
});
afterEach(() => {
sandbox.restore();
});

it('uses self signed JWT when no scopes are provided', async () => {
const jwt = new JWT({
email: 'foo@serviceaccount.com',
key: fs.readFileSync(PEM_PATH, 'utf8'),
scopes: [],
subject: 'bar@subjectaccount.com',
});
jwt.credentials = {refresh_token: 'jwt-placeholder'};
await jwt.getRequestHeaders('https//beepboop.googleapis.com');
sandbox.assert.calledOnce(stubJWTAccess);
});

it('uses self signed JWT when default scopes are provided', async () => {
const jwt = new JWT({
email: 'foo@serviceaccount.com',
key: fs.readFileSync(PEM_PATH, 'utf8'),
defaultScopes: ['http://bar', 'http://foo'],
subject: 'bar@subjectaccount.com',
});
jwt.credentials = {refresh_token: 'jwt-placeholder'};
await jwt.getRequestHeaders('https//beepboop.googleapis.com');
sandbox.assert.calledOnce(stubJWTAccess);
});

it('does not use self signed JWT if target_audience provided', async () => {
const keys = keypair(512 /* bitsize of private key */);
const jwt = new JWT({
email: 'foo@serviceaccount.com',
key: keys.private,
subject: 'ignored@subjectaccount.com',
defaultScopes: ['foo', 'bar'],
additionalClaims: {target_audience: 'beepboop'},
});
jwt.credentials = {refresh_token: 'jwt-placeholder'};
const testUri = 'http:/example.com/my_test_service';
const scope = createGTokenMock({id_token: 'abc123'});
await jwt.getRequestHeaders(testUri);
scope.done();
sandbox.assert.notCalled(stubJWTAccess);
});
// TODO: add tests for defaultScopes being used.
// TODO: add test for request being made with expired JWT.
});
});