Skip to content

fix(auth): Using the App-level http.Agent when fetching public key certificates #705

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

Merged
merged 3 commits into from
Nov 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions src/auth/auth-api-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,13 @@ export abstract class AbstractAuthRequestHandler {
* @constructor
*/
constructor(app: FirebaseApp) {
if (typeof app !== 'object' || app === null || !('options' in app)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'First argument passed to admin.auth() must be a valid Firebase app instance.',
);
}

this.projectId = utils.getProjectId(app);
this.httpClient = new AuthorizedHttpClient(app);
}
Expand Down
42 changes: 12 additions & 30 deletions src/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import {UserRecord, CreateRequest, UpdateRequest} from './user-record';
import {FirebaseApp} from '../firebase-app';
import {FirebaseTokenGenerator, CryptoSigner, cryptoSignerFromApp} from './token-generator';
import {FirebaseTokenGenerator, cryptoSignerFromApp} from './token-generator';
import {
AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler,
} from './auth-api-request';
Expand Down Expand Up @@ -90,6 +90,7 @@ export interface SessionCookieOptions {
* Base Auth class. Mainly used for user management APIs.
*/
export class BaseAuth<T extends AbstractAuthRequestHandler> {

protected readonly tokenGenerator: FirebaseTokenGenerator;
protected readonly idTokenVerifier: FirebaseTokenVerifier;
protected readonly sessionCookieVerifier: FirebaseTokenVerifier;
Expand All @@ -104,12 +105,14 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> {
* minting.
* @constructor
*/
constructor(protected readonly projectId: string,
protected readonly authRequestHandler: T,
cryptoSigner: CryptoSigner) {
constructor(app: FirebaseApp, protected readonly authRequestHandler: T) {
const cryptoSigner = cryptoSignerFromApp(app);
this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner);
this.sessionCookieVerifier = createSessionCookieVerifier(projectId);
this.idTokenVerifier = createIdTokenVerifier(projectId);

const projectId = utils.getProjectId(app);
const httpAgent = app.options.httpAgent;
this.sessionCookieVerifier = createSessionCookieVerifier(projectId, httpAgent);
this.idTokenVerifier = createIdTokenVerifier(projectId, httpAgent);
}

/**
Expand Down Expand Up @@ -617,10 +620,7 @@ export class TenantAwareAuth extends BaseAuth<TenantAwareAuthRequestHandler> {
* @constructor
*/
constructor(app: FirebaseApp, tenantId: string) {
super(
utils.getProjectId(app),
new TenantAwareAuthRequestHandler(app, tenantId),
cryptoSignerFromApp(app));
super(app, new TenantAwareAuthRequestHandler(app, tenantId));
utils.addReadonlyGetter(this, 'tenantId', tenantId);
}

Expand Down Expand Up @@ -721,35 +721,17 @@ export class TenantAwareAuth extends BaseAuth<TenantAwareAuthRequestHandler> {
* An Auth instance can have multiple tenants.
*/
export class Auth extends BaseAuth<AuthRequestHandler> implements FirebaseServiceInterface {

public INTERNAL: AuthInternals = new AuthInternals();
private readonly tenantManager_: TenantManager;
private readonly app_: FirebaseApp;

/**
* Returns the FirebaseApp's project ID.
*
* @param {FirebaseApp} app The project ID for an app.
* @return {string} The FirebaseApp's project ID.
*/
private static getProjectId(app: FirebaseApp): string {
if (typeof app !== 'object' || app === null || !('options' in app)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'First argument passed to admin.auth() must be a valid Firebase app instance.',
);
}
return utils.getProjectId(app);
}

/**
* @param {object} app The app for this Auth service.
* @constructor
*/
constructor(app: FirebaseApp) {
super(
Auth.getProjectId(app),
new AuthRequestHandler(app),
cryptoSignerFromApp(app));
super(app, new AuthRequestHandler(app));
this.app_ = app;
this.tenantManager_ = new TenantManager(app);
}
Expand Down
13 changes: 10 additions & 3 deletions src/auth/token-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as validator from '../utils/validator';
import * as jwt from 'jsonwebtoken';
import { HttpClient, HttpRequestConfig, HttpError } from '../utils/api-request';
import { DecodedIdToken } from './auth';
import { Agent } from 'http';

// Audience to use for Firebase Auth Custom tokens
const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit';
Expand Down Expand Up @@ -75,7 +76,8 @@ export class FirebaseTokenVerifier {

constructor(private clientCertUrl: string, private algorithm: string,
private issuer: string, private projectId: string,
private tokenInfo: FirebaseTokenInfo) {
private tokenInfo: FirebaseTokenInfo,
private readonly httpAgent?: Agent) {
if (!validator.isURL(clientCertUrl)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
Expand Down Expand Up @@ -282,6 +284,7 @@ export class FirebaseTokenVerifier {
const request: HttpRequestConfig = {
method: 'GET',
url: this.clientCertUrl,
httpAgent: this.httpAgent,
};
return client.send(request).then((resp) => {
if (!resp.isJson() || resp.data.error) {
Expand Down Expand Up @@ -325,30 +328,34 @@ export class FirebaseTokenVerifier {
* Creates a new FirebaseTokenVerifier to verify Firebase ID tokens.
*
* @param {string} projectId Project ID string.
* @param {Agent} httpAgent Optional HTTP agent.
* @return {FirebaseTokenVerifier}
*/
export function createIdTokenVerifier(projectId: string): FirebaseTokenVerifier {
export function createIdTokenVerifier(projectId: string, httpAgent?: Agent): FirebaseTokenVerifier {
return new FirebaseTokenVerifier(
CLIENT_CERT_URL,
ALGORITHM_RS256,
'https://securetoken.google.com/',
projectId,
ID_TOKEN_INFO,
httpAgent,
);
}

/**
* Creates a new FirebaseTokenVerifier to verify Firebase session cookies.
*
* @param {string} projectId Project ID string.
* @param {Agent} httpAgent Optional HTTP agent.
* @return {FirebaseTokenVerifier}
*/
export function createSessionCookieVerifier(projectId: string): FirebaseTokenVerifier {
export function createSessionCookieVerifier(projectId: string, httpAgent?: Agent): FirebaseTokenVerifier {
return new FirebaseTokenVerifier(
SESSION_COOKIE_CERT_URL,
ALGORITHM_RS256,
'https://session.firebase.google.com/',
projectId,
SESSION_COOKIE_INFO,
httpAgent,
);
}
23 changes: 23 additions & 0 deletions test/unit/auth/token-verifier.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,29 @@ describe('FirebaseTokenVerifier', () => {
});
});

it('should use the given HTTP Agent', () => {
const agent = new https.Agent();
tokenVerifier = new verifier.FirebaseTokenVerifier(
'https://www.googleapis.com/robot/v1/metadata/x509/[email protected]',
'RS256',
'https://securetoken.google.com/',
'project_id',
verifier.ID_TOKEN_INFO,
agent,
);
mockedRequests.push(mockFetchPublicKeys());

clock = sinon.useFakeTimers(1000);

const mockIdToken = mocks.generateIdToken();

return tokenVerifier.verifyJWT(mockIdToken)
.then(() => {
expect(https.request).to.have.been.calledOnce;
expect(httpsSpy.args[0][0].agent).to.equal(agent);
});
});

it('should not fetch the Google cert public keys until the first time verifyJWT() is called', () => {
mockedRequests.push(mockFetchPublicKeys());

Expand Down