Skip to content

Commit d9df9a9

Browse files
committed
feat: update code and tests
1 parent 95003a5 commit d9df9a9

6 files changed

Lines changed: 123 additions & 29 deletions

File tree

src/main/java/com/google/firebase/fpnv/FirebasePnvException.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,4 @@ public FirebasePnvException(
5757
public FirebasePnvErrorCode getFpnvErrorCode() {
5858
return errorCode;
5959
}
60-
}
60+
}

src/main/java/com/google/firebase/fpnv/FirebasePnvToken.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import com.google.common.collect.ImmutableList;
2222
import com.google.common.collect.ImmutableMap;
23+
import com.nimbusds.jwt.JWTClaimsSet;
2324
import java.util.List;
2425
import java.util.Map;
2526

@@ -29,6 +30,11 @@
2930
public class FirebasePnvToken {
3031
private final Map<String, Object> claims;
3132

33+
/**
34+
* Create an instance of {@link FirebasePnvToken} from {@link JWTClaimsSet} claims.
35+
*
36+
* @param claims Map claims.
37+
*/
3238
public FirebasePnvToken(Map<String, Object> claims) {
3339
checkArgument(claims != null && claims.containsKey("sub"),
3440
"Claims map must at least contain sub");
@@ -71,14 +77,14 @@ public List<String> getAudience() {
7177
* Returns the expiration time in seconds since the Unix epoch.
7278
*/
7379
public long getExpirationTime() {
74-
return ((java.util.Date) claims.get("exp")).getTime();
80+
return ((java.util.Date) claims.get("exp")).getTime() / 1000L;
7581
}
7682

7783
/**
7884
* Returns the issued-at time in seconds since the Unix epoch.
7985
*/
8086
public long getIssuedAt() {
81-
return ((java.util.Date) claims.get("iat")).getTime();
87+
return ((java.util.Date) claims.get("iat")).getTime() / 1000L;
8288
}
8389

8490
/**
@@ -87,4 +93,4 @@ public long getIssuedAt() {
8793
public Map<String, Object> getClaims() {
8894
return claims;
8995
}
90-
}
96+
}

src/main/java/com/google/firebase/fpnv/internal/FirebasePnvTokenVerifier.java

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import java.net.MalformedURLException;
4141
import java.net.URL;
4242
import java.text.ParseException;
43-
import java.util.Date;
4443
import java.util.Objects;
4544

4645
/**
@@ -64,11 +63,11 @@ public FirebasePnvTokenVerifier(FirebaseApp app) {
6463
}
6564

6665
/**
67-
* Main method that do.
68-
* - Explicitly verify the header
69-
* - Verify Signature and Structure
70-
* - Verify Claims (Issuer, Audience, Expiration)
71-
* - Construct Token Object
66+
* Main method that performs the following verification steps:
67+
* - Explicitly verifies the header
68+
* - Verifies signature and structure
69+
* - Verifies claims (e.g. issuer, audience, expiration)
70+
* - Constructs a token object upon successful verification
7271
*
7372
* @param token String input data
7473
* @return {@link FirebasePnvToken}
@@ -104,10 +103,18 @@ public FirebasePnvToken verifyToken(String token) throws FirebasePnvException {
104103
"FPNV token has expired.",
105104
e
106105
);
107-
} catch (BadJOSEException | JOSEException e) {
108-
throw new FirebasePnvException(FirebasePnvErrorCode.SERVICE_ERROR,
106+
} catch (BadJOSEException e) {
107+
throw new FirebasePnvException(
108+
FirebasePnvErrorCode.INVALID_TOKEN,
109109
"Check your project: " + projectId + ". "
110-
+ e.getMessage(),
110+
+ "FPNV token is invalid: " + e.getMessage(),
111+
e
112+
);
113+
} catch (JOSEException e) {
114+
throw new FirebasePnvException(
115+
FirebasePnvErrorCode.INTERNAL_ERROR,
116+
"Check your project: " + projectId + ". "
117+
+ "Failed to verify FPNV token signature: " + e.getMessage(),
111118
e
112119
);
113120
}
@@ -121,7 +128,6 @@ private void verifyHeader(JWSHeader header) throws FirebasePnvException {
121128
"FPNV has incorrect 'algorithm'. Expected " + JWSAlgorithm.ES256.getName()
122129
+ " but got " + header.getAlgorithm());
123130
}
124-
125131
// Check Key ID (kid)
126132
if (Strings.isNullOrEmpty(header.getKeyID())) {
127133
throw new FirebasePnvException(
@@ -141,6 +147,7 @@ private void verifyHeader(JWSHeader header) throws FirebasePnvException {
141147
}
142148

143149
private void verifyClaims(JWTClaimsSet claims) throws FirebasePnvException {
150+
checkArgument(!Objects.isNull(claims), "JWTClaimsSet claims must not be null");
144151
// Verify Issuer
145152
String issuer = claims.getIssuer();
146153

src/test/java/com/google/firebase/fpnv/FirebasePnvErrorCodeTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ public void testEnum() {
3131
assertNotNull(FirebasePnvErrorCode.valueOf("INTERNAL_ERROR"));
3232
assertNotNull(FirebasePnvErrorCode.valueOf("SERVICE_ERROR"));
3333
}
34-
}
34+
}

src/test/java/com/google/firebase/fpnv/FirebasePnvTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,4 @@ public void testVerifyToken_PropagatesException() throws FirebasePnvException {
113113
);
114114
assertEquals(FirebasePnvErrorCode.INVALID_TOKEN, e.getFpnvErrorCode());
115115
}
116-
}
116+
}

src/test/java/com/google/firebase/fpnv/FpnvTokenVerifierTest.java

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.google.firebase.internal.FirebaseProcessEnvironment;
3131
import com.google.firebase.testing.ServiceAccount;
3232
import com.google.firebase.testing.TestUtils;
33+
import com.nimbusds.jose.JOSEException;
3334
import com.nimbusds.jose.JOSEObjectType;
3435
import com.nimbusds.jose.JWSAlgorithm;
3536
import com.nimbusds.jose.JWSHeader;
@@ -39,6 +40,7 @@
3940
import com.nimbusds.jose.jwk.Curve;
4041
import com.nimbusds.jose.jwk.ECKey;
4142
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
43+
import com.nimbusds.jose.proc.BadJOSEException;
4244
import com.nimbusds.jose.proc.SecurityContext;
4345
import com.nimbusds.jwt.JWTClaimsSet;
4446
import com.nimbusds.jwt.SignedJWT;
@@ -56,10 +58,11 @@
5658
import org.mockito.MockitoAnnotations;
5759

5860
public class FpnvTokenVerifierTest {
61+
private static final String PROJECT_ID = "mock-project-id";
5962
private static final FirebaseOptions firebaseOptions = FirebaseOptions.builder()
63+
.setProjectId(PROJECT_ID)
6064
.setCredentials(TestUtils.getCertCredential(ServiceAccount.OWNER.asStream()))
6165
.build();
62-
private static final String PROJECT_ID = "mock-project-id";
6366
private static final String ISSUER = "https://fpnv.googleapis.com/projects/" + PROJECT_ID;
6467
private static final String[] AUD = new String[]{
6568
ISSUER,
@@ -73,6 +76,7 @@ public class FpnvTokenVerifierTest {
7376
private KeyPair rsaKeyPair;
7477
private ECKey ecKey;
7578
private JWSHeader header;
79+
private JWTClaimsSet claims;
7680

7781
@Before
7882
public void setUp() throws Exception {
@@ -99,6 +103,15 @@ public void setUp() throws Exception {
99103
.keyID(ecKey.getKeyID())
100104
.type(JOSEObjectType.JWT)
101105
.build();
106+
107+
// Create a valid JWTClaimsSet
108+
claims = new JWTClaimsSet.Builder()
109+
.issuer(ISSUER)
110+
.audience(Arrays.asList(AUD))
111+
.subject("+15551234567")
112+
.issueTime(new Date())
113+
.expirationTime(new Date(System.currentTimeMillis() + 10000))
114+
.build();
102115
}
103116

104117
@After
@@ -124,18 +137,15 @@ private String createToken(JWSHeader header, JWTClaimsSet claims) throws Excepti
124137
}
125138

126139
@Test
127-
public void testVerifyToken_Success() throws Exception {
128-
Date now = new Date();
129-
Date exp = new Date(now.getTime() + 3600 * 1000); // 1 hour valid
130-
131-
JWTClaimsSet claims = new JWTClaimsSet.Builder()
132-
.issuer(ISSUER)
133-
.audience(Arrays.asList(AUD))
134-
.subject("+15551234567")
135-
.issueTime(now)
136-
.expirationTime(exp)
137-
.build();
140+
public void testVerifyToken_NullOrEmptyToken() {
141+
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () ->
142+
verifier.verifyToken("")
143+
);
144+
assertTrue(e.getMessage().contains("FPNV token must not be null"));
145+
}
138146

147+
@Test
148+
public void testVerifyToken_Success() throws Exception {
139149
String tokenString = createToken(header, claims);
140150

141151
// 1. Mock the processor to return these claims (skipping real signature verification)
@@ -167,6 +177,25 @@ public void testVerifyToken_Header_WrongAlgorithm() throws Exception {
167177
assertTrue(e.getMessage().contains("algorithm"));
168178
}
169179

180+
@Test
181+
public void testVerifyToken_Header_WrongTyp() throws Exception {
182+
JWSHeader header = new JWSHeader
183+
.Builder(JWSAlgorithm.ES256)
184+
.keyID(ecKey.getKeyID())
185+
.type(JOSEObjectType.JOSE)
186+
.build();
187+
JWTClaimsSet claims = new JWTClaimsSet.Builder().build();
188+
189+
String tokenString = createToken(header, claims);
190+
191+
FirebasePnvException e = assertThrows(FirebasePnvException.class, () ->
192+
verifier.verifyToken(tokenString)
193+
);
194+
195+
assertEquals(FirebasePnvErrorCode.INVALID_ARGUMENT, e.getFpnvErrorCode());
196+
assertTrue(e.getMessage().contains("has incorrect 'typ'"));
197+
}
198+
170199
@Test
171200
public void testVerifyToken_Header_MissingKeyId() throws Exception {
172201
// ES256 but missing 'kid'
@@ -195,7 +224,6 @@ public void testVerifyToken_Claims_Expired() throws Exception {
195224
String tokenString = createToken(header, claims);
196225
ExpiredJWTException error = new ExpiredJWTException("Bad token");
197226

198-
// Mock processor returning the expired claims
199227
when(mockJwtProcessor.process(any(SignedJWT.class), any())).thenThrow(error);
200228

201229
FirebasePnvException e = assertThrows(FirebasePnvException.class, () ->
@@ -243,4 +271,57 @@ public void testVerifyToken_Claims_NoSubject() throws Exception {
243271
assertEquals(FirebasePnvErrorCode.INVALID_TOKEN, e.getFpnvErrorCode());
244272
assertTrue(e.getMessage().contains("Token has an empty 'sub' (phone number)"));
245273
}
274+
275+
@Test
276+
public void testVerifyToken_ParseException() {
277+
FirebasePnvException e = assertThrows(FirebasePnvException.class, () ->
278+
verifier.verifyToken(" ")
279+
);
280+
assertEquals(FirebasePnvErrorCode.INVALID_TOKEN, e.getFpnvErrorCode());
281+
assertTrue(e.getMessage().contains("Failed to parse JWT token"));
282+
}
283+
284+
@Test
285+
public void testVerifyToken_BadJOSEException() throws Exception {
286+
String tokenString = createToken(header, claims);
287+
String errorMessage = "BadJOSEException";
288+
BadJOSEException error = new BadJOSEException(errorMessage);
289+
290+
when(mockJwtProcessor.process(any(SignedJWT.class), any())).thenThrow(error);
291+
292+
FirebasePnvException e = assertThrows(FirebasePnvException.class, () ->
293+
verifier.verifyToken(tokenString)
294+
);
295+
296+
assertEquals(FirebasePnvErrorCode.INVALID_TOKEN, e.getFpnvErrorCode());
297+
assertEquals(
298+
"Check your project: "
299+
+ PROJECT_ID
300+
+ ". FPNV token is invalid: "
301+
+ errorMessage,
302+
e.getMessage()
303+
);
304+
}
305+
306+
@Test
307+
public void testVerifyToken_JOSEException() throws Exception {
308+
String tokenString = createToken(header, claims);
309+
String errorMessage = "JOSEException";
310+
JOSEException error = new JOSEException(errorMessage);
311+
312+
when(mockJwtProcessor.process(any(SignedJWT.class), any())).thenThrow(error);
313+
314+
FirebasePnvException e = assertThrows(FirebasePnvException.class, () ->
315+
verifier.verifyToken(tokenString)
316+
);
317+
318+
assertEquals(FirebasePnvErrorCode.INTERNAL_ERROR, e.getFpnvErrorCode());
319+
assertEquals(
320+
"Check your project: "
321+
+ PROJECT_ID
322+
+ ". Failed to verify FPNV token signature: "
323+
+ errorMessage,
324+
e.getMessage()
325+
);
326+
}
246327
}

0 commit comments

Comments
 (0)