-
Notifications
You must be signed in to change notification settings - Fork 301
feat: add FPNV #1170
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
base: main
Are you sure you want to change the base?
feat: add FPNV #1170
Conversation
Summary of ChangesHello @boikoa-gl, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request integrates the Firebase Phone Number Verification (FPNV) feature into the SDK. It provides a new set of APIs and internal logic for verifying FPNV JWTs, allowing applications to securely authenticate users based on phone number verification. This enhancement streamlines the process of validating tokens issued by Firebase's phone authentication services, ensuring data integrity and user trust. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces the Firebase Phone Number Verification (FPNV) feature, including the main service entry point, token and exception types, and the internal token verification logic. The overall structure is sound and follows existing patterns in the Firebase Admin SDK.
However, I've identified several critical issues, particularly concerning type safety in the FirebasePnvToken class that will lead to ClassCastException at runtime. There are also security-related gaps in the token validation logic, such as incomplete issuer verification. Additionally, there are inconsistencies in exception handling and some issues in the test suite that make it less robust.
My detailed comments below provide specific suggestions to address these points, which should be resolved before merging.
| public long getExpirationTime() { | ||
| return (long) claims.get("exp"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method will throw a ClassCastException at runtime. The claims map contains a java.util.Date object for the 'exp' claim from the JWT library, which cannot be directly cast to a long. You should get the Date object and then convert it to seconds since the Unix epoch by calling getTime() / 1000.
| public long getExpirationTime() { | |
| return (long) claims.get("exp"); | |
| } | |
| public long getExpirationTime() { | |
| return ((java.util.Date) claims.get("exp")).getTime() / 1000L; | |
| } |
| public long getIssuedAt() { | ||
| return (long) claims.get("iat"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method will throw a ClassCastException at runtime. The claims map contains a java.util.Date object for the 'iat' claim from the JWT library, which cannot be directly cast to a long. You should get the Date object and then convert it to seconds since the Unix epoch by calling getTime() / 1000.
| public long getIssuedAt() { | |
| return (long) claims.get("iat"); | |
| } | |
| public long getIssuedAt() { | |
| return ((java.util.Date) claims.get("iat")).getTime() / 1000L; | |
| } |
| * Check the error code and message for more | ||
| * details. | ||
| */ | ||
| public class FirebasePnvException extends RuntimeException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This exception extends RuntimeException, but it is declared in the throws clause of FirebasePnv.verifyToken(). This is misleading for users of the API and inconsistent with standard Java practices. To align with other exceptions in the Firebase Admin SDK (like FirebaseAuthException), this should be a checked exception. Please change it to extend Exception.
| public class FirebasePnvException extends RuntimeException { | |
| public class FirebasePnvException extends Exception { |
| public List<String> getAudience() { | ||
| return (List<String>) claims.get("aud"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'aud' (audience) claim in a JWT can be either a single string or a list of strings. This implementation assumes it's always a List<String> and will throw a ClassCastException if the token contains a single string audience. The code should handle both cases gracefully.
| public List<String> getAudience() { | |
| return (List<String>) claims.get("aud"); | |
| } | |
| public List<String> getAudience() { | |
| Object audience = claims.get("aud"); | |
| if (audience instanceof String) { | |
| return com.google.common.collect.ImmutableList.of((String) audience); | |
| } | |
| return (List<String>) audience; | |
| } |
| if (Strings.isNullOrEmpty(issuer)) { | ||
| throw new FirebasePnvException(FirebasePnvErrorCode.INVALID_ARGUMENT, | ||
| "Invalid issuer. Expected: " + issuer); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issuer validation is incomplete. It only checks that the issuer claim is not empty. For security, you must verify that the issuer claim exactly matches the expected value for your service and project ID (e.g., https://fpnv.googleapis.com/projects/YOUR_PROJECT_ID). The current implementation would accept a token from any issuer.
| if (Strings.isNullOrEmpty(issuer)) { | |
| throw new FirebasePnvException(FirebasePnvErrorCode.INVALID_ARGUMENT, | |
| "Invalid issuer. Expected: " + issuer); | |
| } | |
| String expectedIssuer = "https://fpnv.googleapis.com/projects/" + this.projectId; | |
| if (!expectedIssuer.equals(issuer)) { | |
| throw new FirebasePnvException(FirebasePnvErrorCode.INVALID_TOKEN, | |
| String.format("Invalid issuer. Expected: %s but found: %s", expectedIssuer, issuer)); | |
| } |
| private static final String PROJECT_ID = "test-project-123"; | ||
| private static final String ISSUER = "https://fpnv.googleapis.com/projects/" + PROJECT_ID; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PROJECT_ID is hardcoded to "test-project-123", but the FirebaseApp used to initialize the verifier is created with a service account for "mock-project-id". This inconsistency means the verifier instance will have a different project ID than what the tests are asserting against. This makes the issuer validation tests ineffective and they pass only because the implementation is also buggy. The test's project ID should be consistent with the one used by the verifier.
| private static final String PROJECT_ID = "test-project-123"; | |
| private static final String ISSUER = "https://fpnv.googleapis.com/projects/" + PROJECT_ID; | |
| private static final String PROJECT_ID = "mock-project-id"; | |
| private static final String ISSUER = "https://fpnv.googleapis.com/projects/" + PROJECT_ID; |
| public FirebasePnvException( | ||
| FirebasePnvErrorCode authErrorCode, | ||
| String message | ||
| ) { | ||
| super(message); | ||
| this.errorCode = authErrorCode; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameter name authErrorCode is confusing in the context of FPNV (Firebase Phone Number Verification). A more descriptive name like errorCode or fpnvErrorCode would improve clarity.
| public FirebasePnvException( | |
| FirebasePnvErrorCode authErrorCode, | |
| String message | |
| ) { | |
| super(message); | |
| this.errorCode = authErrorCode; | |
| public FirebasePnvException( | |
| FirebasePnvErrorCode errorCode, | |
| String message | |
| ) { | |
| super(message); | |
| this.errorCode = errorCode; | |
| } |
| throw new FirebasePnvException(FirebasePnvErrorCode.SERVICE_ERROR, | ||
| "Chek your project: " + projectId + ". " | ||
| + e.getMessage() | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a typo in the exception message. 'Chek' should be 'Check'.
| throw new FirebasePnvException(FirebasePnvErrorCode.SERVICE_ERROR, | |
| "Chek your project: " + projectId + ". " | |
| + e.getMessage() | |
| ); | |
| throw new FirebasePnvException(FirebasePnvErrorCode.SERVICE_ERROR, | |
| "Check your project: " + projectId + ". " | |
| + e.getMessage() | |
| ); |
| // TODO: i guess this is redundant | ||
| // jwtProcessor.process did this already | ||
| // Verify Expiration | ||
| Date now = new Date(); | ||
| Date exp = claims.getExpirationTime(); | ||
| if (exp == null || now.after(exp)) { | ||
| throw new FirebasePnvException(FirebasePnvErrorCode.TOKEN_EXPIRED, "Token has expired."); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This manual expiration check is redundant. The DefaultJWTProcessor already validates the token's expiration time and will throw an ExpiredJWTException if it has expired. This exception is already being caught and handled in the verifyToken method. You can safely remove this block of code, as the TODO comment suggests.
| @Test | ||
| public void testVerifyToken_Claims_Expired() throws Exception { | ||
| Date past = new Date(System.currentTimeMillis() - 10000); // Expired | ||
|
|
||
| JWTClaimsSet expiredClaims = new JWTClaimsSet.Builder() | ||
| .issuer(ISSUER) | ||
| .audience(ISSUER) | ||
| .subject("+1555") | ||
| .expirationTime(past) | ||
| .build(); | ||
|
|
||
| String tokenString = createToken(header, expiredClaims); | ||
|
|
||
| // Mock processor returning the expired claims | ||
| when(mockJwtProcessor.process(any(SignedJWT.class), any())).thenReturn(expiredClaims); | ||
|
|
||
| FirebasePnvException e = assertThrows(FirebasePnvException.class, () -> | ||
| verifier.verifyToken(tokenString) | ||
| ); | ||
|
|
||
| assertEquals(FirebasePnvErrorCode.TOKEN_EXPIRED, e.getFpnvErrorCode()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test for token expiration is brittle because it relies on an implementation detail in FirebasePnvTokenVerifier (the redundant manual expiration check). Instead of mocking the processor to return expired claims, you should mock it to throw an ExpiredJWTException, which is the expected behavior from the underlying library. This would make the test more robust and independent of the verifier's internal implementation.
Hey there! So you want to contribute to a Firebase SDK?
Before you file this pull request, please read these guidelines:
Discussion
If not, go file an issue about this before creating a pull request to discuss.
Testing
API Changes
us make Firebase APIs better, please propose your change in an issue so that we
can discuss it together.