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

Implemented errors codes for expired ID tokens and session cookies #442

Merged
merged 3 commits into from
Jan 22, 2019
Merged
Show file tree
Hide file tree
Changes from 2 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
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: 45 additions & 10 deletions test/unit/auth/token-verifier.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ 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);
Expand All @@ -46,13 +47,18 @@ const ONE_HOUR_IN_SECONDS = 60 * 60;
/**
* 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): nock.Scope {
hiranya911 marked this conversation as resolved.
Show resolved Hide resolved
const mockedResponse: {[key: string]: string} = {};
mockedResponse[mocks.certificateObject.private_key_id] = mocks.keyPairs[0].public;
if (typeof path === 'undefined') {
path = '/robot/v1/metadata/x509/[email protected]';
}
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 +152,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 +214,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 +234,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 +254,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 +276,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 +416,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