Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for verifying Firebase Phone Number Verification tokens issued in test mode by adding a new 'VerifyPhoneNumberTokenOptions' interface and updating the verification logic to reject test-mode tokens by default unless 'acceptTestTokens' is set to 'true'. The review feedback recommends improving robustness and type safety by using strict inequality when checking the 'acceptTestTokens' option, validating that it is a boolean if provided, and adding corresponding unit tests to cover this validation.
| const projectId = await this.ensureProjectId(); | ||
| const decoded = await this.decodeAndVerify(jwtToken, projectId); | ||
| const decodedIdToken = decoded.payload as PhoneNumberVerificationToken; | ||
| if (decodedIdToken.test_number === true && !options?.acceptTestTokens) { |
There was a problem hiding this comment.
Using a loose truthiness check (!options?.acceptTestTokens) can be risky if a non-boolean truthy value (such as a non-empty string or a number) is passed. It is safer and more robust to use strict inequality (options?.acceptTestTokens !== true) to ensure that test tokens are only accepted when the option is explicitly set to true.
| if (decodedIdToken.test_number === true && !options?.acceptTestTokens) { | |
| if (decodedIdToken.test_number === true && options?.acceptTestTokens !== true) { |
| private validateVerifyPhoneNumberTokenOptions(options?: VerifyPhoneNumberTokenOptions): void { | ||
| if (typeof options === 'undefined') { | ||
| return; | ||
| } | ||
| if (!validator.isNonNullObject(options)) { | ||
| throw new FirebasePhoneNumberVerificationError({ | ||
| code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, | ||
| message: 'VerifyPhoneNumberTokenOptions must be a non-null object.', | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
To prevent potential runtime issues and ensure robust type safety, we should validate that acceptTestTokens is a boolean if it is provided in the options object. This prevents developers from accidentally passing non-boolean truthy values (like strings or numbers) which might otherwise bypass the test token rejection logic.
private validateVerifyPhoneNumberTokenOptions(options?: VerifyPhoneNumberTokenOptions): void {
if (typeof options === 'undefined') {
return;
}
if (!validator.isNonNullObject(options)) {
throw new FirebasePhoneNumberVerificationError({
code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT,
message: 'VerifyPhoneNumberTokenOptions must be a non-null object.',
});
}
if (typeof options.acceptTestTokens !== 'undefined' && !validator.isBoolean(options.acceptTestTokens)) {
throw new FirebasePhoneNumberVerificationError({
code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT,
message: 'acceptTestTokens option must be a boolean.',
});
}
}| it('should throw given invalid VerifyPhoneNumberTokenOptions', () => { | ||
| [null, 100, -100, 'abc', [], true].forEach((invalidOptions) => { | ||
| expect(() => { | ||
| return phoneNumberVerificationService.verifyToken(mockTokenString, invalidOptions as any); | ||
| }).to.throw('VerifyPhoneNumberTokenOptions must be a non-null object.'); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Add test cases to verify that passing a non-boolean value for acceptTestTokens correctly throws an error, matching the new validation logic.
it('should throw given invalid VerifyPhoneNumberTokenOptions', () => {
[null, 100, -100, 'abc', [], true].forEach((invalidOptions) => {
expect(() => {
return phoneNumberVerificationService.verifyToken(mockTokenString, invalidOptions as any);
}).to.throw('VerifyPhoneNumberTokenOptions must be a non-null object.');
});
[123, 'true', [], null].forEach((invalidAcceptTestTokens) => {
expect(() => {
return phoneNumberVerificationService.verifyToken(mockTokenString, { acceptTestTokens: invalidAcceptTestTokens as any });
}).to.throw('acceptTestTokens option must be a boolean.');
});
}…ionTokenOptions
- Add TEST_TOKEN_REJECTED ('test-token-rejected') to PhoneNumberVerificationErrorCode and reject tokens containing '"test_number": true' by default unless options?.acceptTestTokens === true.
- Add and export PhoneNumberVerificationTokenOptions ({ acceptTestTokens?: boolean }) on PhoneNumberVerification.verifyToken(jwt, options?) with runtime validation.
- Populate required testNumber: boolean property on PhoneNumberVerificationToken.
- Update unit tests and API Extractor report.
5243af3 to
f8b505c
Compare
lahirumaramba
left a comment
There was a problem hiding this comment.
Hey @micah-gcp, thank you for the PR! Overall this looks great. I added a few comments on input validation consistency and getting a quick TW review on the new docstrings, let me know what you think!
| jwt: string, | ||
| options?: PhoneNumberVerificationTokenOptions | ||
| ): Promise<PhoneNumberVerificationToken> { | ||
| this.validatePhoneNumberVerificationTokenOptions(options); |
There was a problem hiding this comment.
WDYT about moving validatePhoneNumberVerificationTokenOptions(options) into PhoneNumberTokenVerifier.verifyJWT(), right after the jwt string check? verifyToken() isn't async, so bad options throw synchronously while a bad jwt returns a rejected promise. Moving it keeps both on the promise path and validates jwt first.
| .to.be.rejectedWith(FirebasePhoneNumberVerificationError, /is a test token/) | ||
| .and.eventually.have.property('code', expectedCode); | ||
|
|
||
| await expect(verifier.verifyJWT('test-token', { acceptTestTokens: 'false' as any })) |
There was a problem hiding this comment.
through the public API, { acceptTestTokens: 'false' as any } gives invalid-argument, but calling verifyJWT() directly skips options validation and gives test-token-rejected. With the move above, both reject with INVALID_ARGUMENT.
| /** | ||
| * Interface representing options for the {@link PhoneNumberVerification.verifyToken} method. | ||
| */ | ||
| export interface PhoneNumberVerificationTokenOptions { |
There was a problem hiding this comment.
Could we get a TW review on the new docstrings?
super nit: I know the API proposal settled on PhoneNumberVerificationTokenOptions, but we could also consider VerifyPhoneNumberTokenOptions to mirror VerifyAppCheckTokenOptions if we really want to :)
| [null, 100, -100, 'abc', [], true].forEach((invalidOptions) => { | ||
| expect(() => { | ||
| return phoneNumberVerificationService.verifyToken(mockTokenString, invalidOptions as any); | ||
| }).to.throw('PhoneNumberVerificationTokenOptions must be a non-null object.'); |
There was a problem hiding this comment.
nit: also assert the error is a FirebasePhoneNumberVerificationError with code phone-number-verification/invalid-argument (here and in the acceptTestTokens test below).
| * | ||
| * Enable this option only during development and testing. | ||
| */ | ||
| acceptTestTokens?: boolean; |
There was a problem hiding this comment.
indentation in PhoneNumberVerificationTokenOptions looks off. Could you compare with PhoneNumberVerificationToken, please?
Summary
Updates
PhoneNumberVerification.verifyToken()infirebase-admin/phone-number-verificationto handle test-mode tokens ("test_number": true) safely by default:TEST_TOKEN_REJECTED: AddsTEST_TOKEN_REJECTED: 'test-token-rejected'toPhoneNumberVerificationErrorCode. When a decoded FPNV token contains"test_number": true,verifyToken(jwt, options?)throwsFirebasePhoneNumberVerificationError(phone-number-verification/test-token-rejected) unlessoptions?.acceptTestTokens === true.PhoneNumberVerificationTokenOptions: ExportsPhoneNumberVerificationTokenOptions({ acceptTestTokens?: boolean }) fromfirebase-admin/phone-number-verificationand validates thatoptions, when provided, is a non-null object andacceptTestTokensis a boolean.testNumberonPhoneNumberVerificationToken: Sets the required camelCasetestNumber: booleanconvenience property (decodedIdToken.test_number === true), matching howphoneNumbermirrorssub.Testing
PhoneNumberVerificationErrorCode.TEST_TOKEN_REJECTED(includingerr.hasCode()),PhoneNumberVerificationTokenOptionsvalidation, default test-token rejection,{ acceptTestTokens: true }opt-in, andtestNumberpopulation.etc/firebase-admin.phone-number-verification.api.md) and verifiednpm testandnpm run api-extractor.