Skip to content

Commit

Permalink
Implemented errors codes for expired ID tokens and session cookies (#442
Browse files Browse the repository at this point in the history
)

* Implemented errors codes for expired ID tokens and session cookies

* Handling other possible verify errors

* Refactored out the default arg from helper function
  • Loading branch information
hiranya911 authored Jan 22, 2019
1 parent b6d9af4 commit 750a74b
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 30 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Unreleased

- [changed] `verifyIdToken()` and `verifySessionCookie()` methods now return
`auth/id-token-expired` and `auth/session-cookie-expired` error codes for
expired JWTs.
- [fixed] Implemented a Node.js environment check that will be executed at
package import time.

Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 14 additions & 13 deletions src/auth/token-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

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

import * as validator from '../utils/validator';
import * as jwt from 'jsonwebtoken';
Expand All @@ -38,7 +38,7 @@ export const ID_TOKEN_INFO: FirebaseTokenInfo = {
verifyApiName: 'verifyIdToken()',
jwtName: 'Firebase ID token',
shortName: 'ID token',
expiredErrorCode: 'auth/id-token-expired',
expiredErrorCode: AuthClientErrorCode.ID_TOKEN_EXPIRED,
};

/** User facing token information related to the Firebase session cookie. */
Expand All @@ -47,7 +47,7 @@ export const SESSION_COOKIE_INFO: FirebaseTokenInfo = {
verifyApiName: 'verifySessionCookie()',
jwtName: 'Firebase session cookie',
shortName: 'session cookie',
expiredErrorCode: 'auth/session-cookie-expired',
expiredErrorCode: AuthClientErrorCode.SESSION_COOKIE_EXPIRED,
};

/** Interface that defines token related user facing information. */
Expand All @@ -61,7 +61,7 @@ export interface FirebaseTokenInfo {
/** The JWT short name. */
shortName: string;
/** JWT Expiration error code. */
expiredErrorCode: string;
expiredErrorCode: ErrorInfo;
}

/**
Expand Down Expand Up @@ -115,10 +115,10 @@ export class FirebaseTokenVerifier {
AuthClientErrorCode.INVALID_ARGUMENT,
`The JWT public short name must be a non-empty string.`,
);
} else if (!validator.isNonEmptyString(tokenInfo.expiredErrorCode)) {
} else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || !('code' in tokenInfo.expiredErrorCode)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
`The JWT expiration error code must be a non-empty string.`,
`The JWT expiration error code must be a non-null ErrorInfo object.`,
);
}
this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a';
Expand Down Expand Up @@ -228,22 +228,23 @@ export class FirebaseTokenVerifier {
* verification.
*/
private verifyJwtSignatureWithKey(jwtToken: string, publicKey: string): Promise<object> {
let errorMessage: string;
const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` +
`for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`;
return new Promise((resolve, reject) => {
jwt.verify(jwtToken, publicKey, {
algorithms: [this.algorithm],
}, (error: any, decodedToken: any) => {
}, (error: jwt.VerifyErrors, decodedToken: any) => {
if (error) {
if (error.name === 'TokenExpiredError') {
errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh token from your client ` +
`app and try again (${this.tokenInfo.expiredErrorCode}).` + verifyJwtTokenDocsMessage;
const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` +
` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` +
verifyJwtTokenDocsMessage;
return reject(new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage));
} else if (error.name === 'JsonWebTokenError') {
errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage;
const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage;
return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage));
}

return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage));
return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message));
} else {
decodedToken.uid = decodedToken.sub;
resolve(decodedToken);
Expand Down
4 changes: 4 additions & 0 deletions src/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,10 @@ export class AuthClientErrorCode {
'https://firebase.google.com/docs/admin/setup for details on how to authenticate this SDK ' +
'with appropriate permissions.',
};
public static SESSION_COOKIE_EXPIRED = {
code: 'session-cookie-expired',
message: 'The Firebase session cookie is expired.',
};
public static SESSION_COOKIE_REVOKED = {
code: 'session-cookie-revoked',
message: 'The Firebase session cookie has been revoked.',
Expand Down
55 changes: 43 additions & 12 deletions test/unit/auth/token-verifier.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,29 @@ import {FirebaseTokenGenerator, ServiceAccountSigner} from '../../../src/auth/to
import * as verifier from '../../../src/auth/token-verifier';

import {Certificate} from '../../../src/auth/credential';
import { AuthClientErrorCode } from '../../../src/utils/error';

chai.should();
chai.use(sinonChai);
chai.use(chaiAsPromised);

const expect = chai.expect;


const ONE_HOUR_IN_SECONDS = 60 * 60;

const idTokenPublicCertPath = '/robot/v1/metadata/x509/[email protected]';

/**
* Returns a mocked out success response from the URL containing the public keys for the Google certs.
*
* @param {string=} path URL path to which the mock request should be made. If not specified, defaults
* to the URL path of ID token public key certificates.
* @return {Object} A nock response object.
*/
function mockFetchPublicKeys(): nock.Scope {
function mockFetchPublicKeys(path: string = idTokenPublicCertPath): nock.Scope {
const mockedResponse: {[key: string]: string} = {};
mockedResponse[mocks.certificateObject.private_key_id] = mocks.keyPairs[0].public;
return nock('https://www.googleapis.com')
.get('/robot/v1/metadata/x509/[email protected]')
.get(path)
.reply(200, mockedResponse, {
'cache-control': 'public, max-age=1, must-revalidate, no-transform',
});
Expand Down Expand Up @@ -146,7 +148,7 @@ describe('FirebaseTokenVerifier', () => {
verifyApiName: 'verifyToken()',
jwtName: 'Important Token',
shortName: 'token',
expiredErrorCode: 'auth/important-token-expired',
expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT,
},
);
}).not.to.throw();
Expand Down Expand Up @@ -208,7 +210,7 @@ describe('FirebaseTokenVerifier', () => {
verifyApiName: invalidVerifyApiName as any,
jwtName: 'Important Token',
shortName: 'token',
expiredErrorCode: 'auth/important-token-expired',
expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT,
});
}).to.throw('The JWT verify API name must be a non-empty string.');
});
Expand All @@ -228,7 +230,7 @@ describe('FirebaseTokenVerifier', () => {
verifyApiName: 'verifyToken()',
jwtName: invalidJwtName as any,
shortName: 'token',
expiredErrorCode: 'auth/important-token-expired',
expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT,
});
}).to.throw('The JWT public full name must be a non-empty string.');
});
Expand All @@ -248,13 +250,13 @@ describe('FirebaseTokenVerifier', () => {
verifyApiName: 'verifyToken()',
jwtName: 'Important Token',
shortName: invalidShortName as any,
expiredErrorCode: 'auth/important-token-expired',
expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT,
});
}).to.throw('The JWT public short name must be a non-empty string.');
});
});

const invalidExpiredErrorCodes = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, ''];
const invalidExpiredErrorCodes = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, '', 'test'];
invalidExpiredErrorCodes.forEach((invalidExpiredErrorCode) => {
it('should throw given an invalid expiration error code: ' + JSON.stringify(invalidExpiredErrorCode), () => {
expect(() => {
Expand All @@ -270,7 +272,7 @@ describe('FirebaseTokenVerifier', () => {
shortName: 'token',
expiredErrorCode: invalidExpiredErrorCode as any,
});
}).to.throw('The JWT expiration error code must be a non-empty string.');
}).to.throw('The JWT expiration error code must be a non-null ErrorInfo object.');
});
});
});
Expand Down Expand Up @@ -410,8 +412,37 @@ describe('FirebaseTokenVerifier', () => {

// Token should now be invalid
return tokenVerifier.verifyJWT(mockIdToken)
.should.eventually.be.rejectedWith('Firebase ID token has expired. Get a fresh token from your client ' +
'app and try again (auth/id-token-expired)');
.should.eventually.be.rejectedWith('Firebase ID token has expired. Get a fresh ID token from your client ' +
'app and try again (auth/id-token-expired)')
.and.have.property('code', 'auth/id-token-expired');
});
});

it('should be rejected given an expired Firebase session cookie', () => {
const tokenVerifierSessionCookie = new verifier.FirebaseTokenVerifier(
'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys',
'RS256',
'https://session.firebase.google.com/',
'project_id',
verifier.SESSION_COOKIE_INFO,
);
mockedRequests.push(mockFetchPublicKeys('/identitytoolkit/v3/relyingparty/publicKeys'));

clock = sinon.useFakeTimers(1000);

const mockSessionCookie = mocks.generateSessionCookie();

clock.tick((ONE_HOUR_IN_SECONDS * 1000) - 1);

// Cookie should still be valid
return tokenVerifierSessionCookie.verifyJWT(mockSessionCookie).then(() => {
clock.tick(1);

// Cookie should now be invalid
return tokenVerifierSessionCookie.verifyJWT(mockSessionCookie)
.should.eventually.be.rejectedWith('Firebase session cookie has expired. Get a fresh session cookie from ' +
'your client app and try again (auth/session-cookie-expired).')
.and.have.property('code', 'auth/session-cookie-expired');
});
});

Expand Down

0 comments on commit 750a74b

Please sign in to comment.