Skip to content

Async API to determine project ID #715

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 2 commits into from
Dec 11, 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: 2 additions & 5 deletions src/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,8 @@ export class BaseAuth<T extends AbstractAuthRequestHandler> {
constructor(app: FirebaseApp, protected readonly authRequestHandler: T) {
const cryptoSigner = cryptoSignerFromApp(app);
this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner);

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

/**
Expand Down
51 changes: 27 additions & 24 deletions src/auth/token-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@

import {AuthClientErrorCode, FirebaseAuthError, ErrorInfo} from '../utils/error';

import * as util from '../utils/index';
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';
import { FirebaseApp } from '../firebase-app';

// 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,9 +76,8 @@ export class FirebaseTokenVerifier {
private readonly shortNameArticle: string;

constructor(private clientCertUrl: string, private algorithm: string,
private issuer: string, private projectId: string | null,
private tokenInfo: FirebaseTokenInfo,
private readonly httpAgent?: Agent) {
private issuer: string, private tokenInfo: FirebaseTokenInfo,
private readonly app: FirebaseApp) {
if (!validator.isURL(clientCertUrl)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
Expand Down Expand Up @@ -144,7 +144,14 @@ export class FirebaseTokenVerifier {
);
}

if (!validator.isNonEmptyString(this.projectId)) {
return util.findProjectId(this.app)
.then((projectId) => {
return this.verifyJWTWithProjectId(jwtToken, projectId);
});
}

private verifyJWTWithProjectId(jwtToken: string, projectId: string | null): Promise<DecodedIdToken> {
if (!validator.isNonEmptyString(projectId)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CREDENTIAL,
`Must initialize app with a cert credential or set your Firebase project ID as the ` +
Expand Down Expand Up @@ -186,13 +193,13 @@ export class FirebaseTokenVerifier {
} else if (header.alg !== this.algorithm) {
errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + this.algorithm + `" but got ` +
`"` + header.alg + `".` + verifyJwtTokenDocsMessage;
} else if (payload.aud !== this.projectId) {
} else if (payload.aud !== projectId) {
errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` +
this.projectId + `" but got "` + payload.aud + `".` + projectIdMatchMessage +
projectId + `" but got "` + payload.aud + `".` + projectIdMatchMessage +
verifyJwtTokenDocsMessage;
} else if (payload.iss !== this.issuer + this.projectId) {
} else if (payload.iss !== this.issuer + projectId) {
errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` +
`"${this.issuer}"` + this.projectId + `" but got "` +
`"${this.issuer}"` + projectId + `" but got "` +
payload.iss + `".` + projectIdMatchMessage + verifyJwtTokenDocsMessage;
} else if (typeof payload.sub !== 'string') {
errorMessage = `${this.tokenInfo.jwtName} has no "sub" (subject) claim.` + verifyJwtTokenDocsMessage;
Expand Down Expand Up @@ -284,7 +291,7 @@ export class FirebaseTokenVerifier {
const request: HttpRequestConfig = {
method: 'GET',
url: this.clientCertUrl,
httpAgent: this.httpAgent,
httpAgent: this.app.options.httpAgent,
};
return client.send(request).then((resp) => {
if (!resp.isJson() || resp.data.error) {
Expand Down Expand Up @@ -327,35 +334,31 @@ export class FirebaseTokenVerifier {
/**
* Creates a new FirebaseTokenVerifier to verify Firebase ID tokens.
*
* @param {string} projectId Project ID string.
* @param {Agent} httpAgent Optional HTTP agent.
* @param {FirebaseApp} app Firebase app instance.
* @return {FirebaseTokenVerifier}
*/
export function createIdTokenVerifier(projectId: string | null, httpAgent?: Agent): FirebaseTokenVerifier {
export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier {
return new FirebaseTokenVerifier(
CLIENT_CERT_URL,
ALGORITHM_RS256,
'https://securetoken.google.com/',
projectId,
ID_TOKEN_INFO,
httpAgent,
CLIENT_CERT_URL,
ALGORITHM_RS256,
'https://securetoken.google.com/',
ID_TOKEN_INFO,
app,
);
}

/**
* Creates a new FirebaseTokenVerifier to verify Firebase session cookies.
*
* @param {string} projectId Project ID string.
* @param {Agent} httpAgent Optional HTTP agent.
* @param {FirebaseApp} app Firebase app instance.
* @return {FirebaseTokenVerifier}
*/
export function createSessionCookieVerifier(projectId: string | null, httpAgent?: Agent): FirebaseTokenVerifier {
export function createSessionCookieVerifier(app: FirebaseApp): FirebaseTokenVerifier {
return new FirebaseTokenVerifier(
SESSION_COOKIE_CERT_URL,
ALGORITHM_RS256,
'https://session.firebase.google.com/',
projectId,
SESSION_COOKIE_INFO,
httpAgent,
app,
);
}
14 changes: 14 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,20 @@ export function getProjectId(app: FirebaseApp): string | null {
return null;
}

/**
* Determines the Google Cloud project ID associated with a Firebase app by examining
* the Firebase app options, credentials and the local environment in that order. This
* is an async wrapper of the getProjectId method. This enables us to migrate the rest
* of the SDK into asynchronously determining the current project ID. See b/143090254.
*
* @param {FirebaseApp} app A Firebase app to get the project ID from.
*
* @return {Promise<string | null>} A project ID string or null.
*/
export function findProjectId(app: FirebaseApp): Promise<string | null> {
return Promise.resolve(getProjectId(app));
}

/**
* Encodes data using web-safe-base64.
*
Expand Down
14 changes: 6 additions & 8 deletions test/unit/auth/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,22 +387,20 @@ AUTH_CONFIGS.forEach((testConfig) => {
}
});

it('verifyIdToken() should throw when project ID is not specified', () => {
it('verifyIdToken() should reject when project ID is not specified', () => {
const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp());
const expected = 'Must initialize app with a cert credential or set your Firebase project ID ' +
'as the GOOGLE_CLOUD_PROJECT environment variable to call verifyIdToken().';
expect(() => {
mockCredentialAuth.verifyIdToken(mocks.generateIdToken());
}).to.throw(expected);
return mockCredentialAuth.verifyIdToken(mocks.generateIdToken())
.should.eventually.be.rejectedWith(expected);
});

it('verifySessionCookie() should throw when project ID is not specified', () => {
it('verifySessionCookie() should reject when project ID is not specified', () => {
const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp());
const expected = 'Must initialize app with a cert credential or set your Firebase project ID ' +
'as the GOOGLE_CLOUD_PROJECT environment variable to call verifySessionCookie().';
expect(() => {
mockCredentialAuth.verifySessionCookie(mocks.generateSessionCookie());
}).to.throw(expected);
return mockCredentialAuth.verifySessionCookie(mocks.generateSessionCookie())
.should.eventually.be.rejectedWith(expected);
});

describe('verifyIdToken()', () => {
Expand Down
Loading