Skip to content

Commit a727f6c

Browse files
authored
Reading IID Token from IID Shared Prefs. (#1095)
1 parent 6ae83de commit a727f6c

File tree

5 files changed

+114
-22
lines changed

5 files changed

+114
-22
lines changed

firebase-installations/api.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ package com.google.firebase.installations.local {
3232
public class IidStore {
3333
ctor public IidStore();
3434
method @Nullable public String readIid();
35+
method @Nullable public String readToken();
3536
}
3637

3738
public class PersistedInstallation {
@@ -92,7 +93,7 @@ package com.google.firebase.installations.remote {
9293

9394
public class FirebaseInstallationServiceClient {
9495
ctor public FirebaseInstallationServiceClient(@NonNull android.content.Context, @Nullable com.google.firebase.platforminfo.UserAgentPublisher, @Nullable com.google.firebase.heartbeatinfo.HeartBeatInfo);
95-
method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws java.io.IOException;
96+
method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String, @Nullable String) throws java.io.IOException;
9697
method @NonNull public void deleteFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.FirebaseException, java.io.IOException;
9798
method @NonNull public com.google.firebase.installations.remote.TokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws java.io.IOException;
9899
}

firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,8 @@ private final void doRegistrationInternal(boolean forceRefresh) {
307307
* been persisted.
308308
*/
309309
private PersistedInstallationEntry getPrefsWithGeneratedIdMultiProcessSafe() {
310-
CrossProcessLock lock = CrossProcessLock
311-
.acquire(firebaseApp.getApplicationContext(), LOCKFILE_NAME_GENERATE_FID);
310+
CrossProcessLock lock =
311+
CrossProcessLock.acquire(firebaseApp.getApplicationContext(), LOCKFILE_NAME_GENERATE_FID);
312312
try {
313313
synchronized (lockGenerateFid) {
314314
PersistedInstallationEntry prefs =
@@ -354,7 +354,8 @@ private PersistedInstallationEntry registerFidWithServer(PersistedInstallationEn
354354
/*apiKey= */ firebaseApp.getOptions().getApiKey(),
355355
/*fid= */ prefs.getFirebaseInstallationId(),
356356
/*projectID= */ firebaseApp.getOptions().getProjectId(),
357-
/*appId= */ getApplicationId());
357+
/*appId= */ getApplicationId(),
358+
/* migration-header= */ null);
358359

359360
switch (response.getResponseCode()) {
360361
case OK:

firebase-installations/src/main/java/com/google/firebase/installations/local/IidStore.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import java.security.PublicKey;
3131
import java.security.spec.InvalidKeySpecException;
3232
import java.security.spec.X509EncodedKeySpec;
33+
import org.json.JSONException;
34+
import org.json.JSONObject;
3335

3436
/**
3537
* Read existing iid only for default (first initialized) instance of this firebase application.*
@@ -38,17 +40,79 @@ public class IidStore {
3840
private static final String IID_SHARED_PREFS_NAME = "com.google.android.gms.appid";
3941
private static final String STORE_KEY_PUB = "|S||P|";
4042
private static final String STORE_KEY_ID = "|S|id";
43+
private static final String STORE_KEY_TOKEN = "|T|";
44+
private static final String DEFAULT_SCOPE = "|*";
45+
private static final String JSON_TOKEN_KEY = "token";
46+
private static final String JSON_ENCODED_PREFIX = "{";
4147

4248
@GuardedBy("iidPrefs")
4349
private final SharedPreferences iidPrefs;
4450

51+
private final String defaultSenderId;
52+
4553
public IidStore() {
4654
// Different FirebaseApp in the same Android application should have the same application
4755
// context and same dir path. We only read existing Iids for the default firebase application.
4856
iidPrefs =
4957
FirebaseApp.getInstance()
5058
.getApplicationContext()
5159
.getSharedPreferences(IID_SHARED_PREFS_NAME, Context.MODE_PRIVATE);
60+
61+
defaultSenderId = getDefaultSenderId(FirebaseApp.getInstance());
62+
}
63+
64+
private static String getDefaultSenderId(FirebaseApp app) {
65+
// Check for an explicit sender id
66+
String senderId = app.getOptions().getGcmSenderId();
67+
if (senderId != null) {
68+
return senderId;
69+
}
70+
String appId = app.getOptions().getApplicationId();
71+
if (!appId.startsWith("1:") && !appId.startsWith("2:")) {
72+
// If applicationId does not contain a (GMP-)App-ID, it contains a Sender identifier
73+
return appId;
74+
}
75+
// For v1 app IDs, fall back to parsing the project number out
76+
@SuppressWarnings("StringSplitter")
77+
String[] parts = appId.split(":");
78+
if (parts.length != 4) {
79+
return null; // Invalid format
80+
}
81+
String projectNumber = parts[1];
82+
if (projectNumber.isEmpty()) {
83+
return null; // No project number
84+
}
85+
return projectNumber;
86+
}
87+
88+
private String createTokenKey(@NonNull String senderId) {
89+
return STORE_KEY_TOKEN + senderId + DEFAULT_SCOPE;
90+
}
91+
92+
@Nullable
93+
public String readToken() {
94+
synchronized (iidPrefs) {
95+
String token = iidPrefs.getString(createTokenKey(defaultSenderId), null);
96+
if (token.isEmpty()) {
97+
return null;
98+
}
99+
100+
if (token.startsWith(JSON_ENCODED_PREFIX)) {
101+
return parseIidTokenFromJson(token);
102+
}
103+
// Legacy value, token is whole string
104+
return token;
105+
}
106+
}
107+
108+
private String parseIidTokenFromJson(String token) {
109+
// Encoded as JSON
110+
try {
111+
JSONObject json = new JSONObject(token);
112+
return json.getString(JSON_TOKEN_KEY);
113+
} catch (JSONException e) {
114+
return null;
115+
}
52116
}
53117

54118
@Nullable

firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ public class FirebaseInstallationServiceClient {
7070
private static final String X_ANDROID_PACKAGE_HEADER_KEY = "X-Android-Package";
7171
private static final String X_ANDROID_CERT_HEADER_KEY = "X-Android-Cert";
7272

73+
private static final String X_ANDROID_IID_MIGRATION_KEY = "x-goog-fis-android-iid-migration-auth";
74+
7375
private static final int NETWORK_TIMEOUT_MILLIS = 10000;
7476

7577
private static final Pattern EXPIRATION_TIMESTAMP_PATTERN = Pattern.compile("[0-9]+s");
@@ -87,10 +89,10 @@ public class FirebaseInstallationServiceClient {
8789
public FirebaseInstallationServiceClient(
8890
@NonNull Context context,
8991
@Nullable UserAgentPublisher publisher,
90-
@Nullable HeartBeatInfo heartBeatInfo) {
92+
@Nullable HeartBeatInfo heartbeatInfo) {
9193
this.context = context;
9294
this.userAgentPublisher = publisher;
93-
this.heartbeatInfo = heartBeatInfo;
95+
this.heartbeatInfo = heartbeatInfo;
9496
}
9597

9698
/**
@@ -100,6 +102,8 @@ public FirebaseInstallationServiceClient(
100102
* @param fid Firebase Installation Identifier
101103
* @param projectID Project Id
102104
* @param appId the identifier of a Firebase application
105+
* @param iidToken the identifier token of a Firebase application with instance id. It is set to
106+
* null for a FID.
103107
* @return {@link InstallationResponse} generated from the response body
104108
* <ul>
105109
* <li>400: return response with status BAD_CONFIG
@@ -111,7 +115,11 @@ public FirebaseInstallationServiceClient(
111115
*/
112116
@NonNull
113117
public InstallationResponse createFirebaseInstallation(
114-
@NonNull String apiKey, @NonNull String fid, @NonNull String projectID, @NonNull String appId)
118+
@NonNull String apiKey,
119+
@NonNull String fid,
120+
@NonNull String projectID,
121+
@NonNull String appId,
122+
@Nullable String iidToken)
115123
throws IOException {
116124
String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectID);
117125
int retryCount = 0;
@@ -128,6 +136,11 @@ public InstallationResponse createFirebaseInstallation(
128136
httpsURLConnection.setRequestMethod("POST");
129137
httpsURLConnection.setDoOutput(true);
130138

139+
// Note: Set the iid token header for authenticating the Instance-ID migrating to FIS.
140+
if (iidToken != null) {
141+
httpsURLConnection.addRequestProperty(X_ANDROID_IID_MIGRATION_KEY, iidToken);
142+
}
143+
131144
GZIPOutputStream gzipOutputStream =
132145
new GZIPOutputStream(httpsURLConnection.getOutputStream());
133146
try {

firebase-installations/src/test/java/com/google/firebase/installations/FirebaseInstallationsTest.java

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.junit.Assert.assertThat;
2121
import static org.junit.Assert.assertTrue;
2222
import static org.junit.Assert.fail;
23+
import static org.mockito.ArgumentMatchers.any;
2324
import static org.mockito.ArgumentMatchers.anyString;
2425
import static org.mockito.ArgumentMatchers.matches;
2526
import static org.mockito.Mockito.doAnswer;
@@ -178,7 +179,8 @@ public void cleanUp() {
178179
*/
179180
@Test
180181
public void testGetId_noNetwork_noIid() throws Exception {
181-
when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
182+
when(mockBackend.createFirebaseInstallation(
183+
anyString(), anyString(), anyString(), anyString(), any()))
182184
.thenThrow(new IOException());
183185
when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString()))
184186
.thenThrow(new IOException());
@@ -207,7 +209,8 @@ public void testGetId_noNetwork_noIid() throws Exception {
207209

208210
@Test
209211
public void testGetId_noNetwork_iidPresent() throws Exception {
210-
when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
212+
when(mockBackend.createFirebaseInstallation(
213+
anyString(), anyString(), anyString(), anyString(), any()))
211214
.thenThrow(new IOException());
212215
when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString()))
213216
.thenThrow(new IOException());
@@ -236,7 +239,8 @@ public void testGetId_noNetwork_iidPresent() throws Exception {
236239

237240
@Test
238241
public void testGetId_noNetwork_fidAlreadyGenerated() throws Exception {
239-
when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
242+
when(mockBackend.createFirebaseInstallation(
243+
anyString(), anyString(), anyString(), anyString(), any()))
240244
.thenThrow(new IOException());
241245
when(mockBackend.generateAuthToken(anyString(), anyString(), anyString(), anyString()))
242246
.thenThrow(new IOException());
@@ -303,7 +307,7 @@ public void testGetId_ValidIdAndToken_NoBackendCalls() throws Exception {
303307
@Test
304308
public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception {
305309
when(mockBackend.createFirebaseInstallation(
306-
anyString(), matches(TEST_FID_1), anyString(), anyString()))
310+
anyString(), matches(TEST_FID_1), anyString(), anyString(), any()))
307311
.thenReturn(TEST_INSTALLATION_RESPONSE);
308312
persistedInstallation.insertOrUpdatePersistedInstallationEntry(
309313
PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1));
@@ -322,7 +326,8 @@ public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception {
322326
// check that the mockClient didn't get invoked at all, since the fid is already registered
323327
// and the authtoken is present and not expired
324328
verify(mockBackend)
325-
.createFirebaseInstallation(anyString(), matches(TEST_FID_1), anyString(), anyString());
329+
.createFirebaseInstallation(
330+
anyString(), matches(TEST_FID_1), anyString(), anyString(), any());
326331
verify(mockBackend, never())
327332
.generateAuthToken(anyString(), anyString(), anyString(), anyString());
328333

@@ -335,7 +340,8 @@ public void testGetId_UnRegisteredId_IssueCreateIdCall() throws Exception {
335340
@Test
336341
public void testGetId_migrateIid_successful() throws Exception {
337342
when(mockIidStore.readIid()).thenReturn(TEST_INSTANCE_ID_1);
338-
when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
343+
when(mockBackend.createFirebaseInstallation(
344+
anyString(), anyString(), anyString(), anyString(), any()))
339345
.thenReturn(TEST_INSTALLATION_RESPONSE_WITH_IID);
340346

341347
// Do the actual getId() call under test.
@@ -361,7 +367,8 @@ public void testGetId_migrateIid_successful() throws Exception {
361367
@Test
362368
public void testGetId_multipleCalls_sameFIDReturned() throws Exception {
363369
when(mockIidStore.readIid()).thenReturn(null);
364-
when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
370+
when(mockBackend.createFirebaseInstallation(
371+
anyString(), anyString(), anyString(), anyString(), any()))
365372
.thenReturn(TEST_INSTALLATION_RESPONSE);
366373

367374
// Call getId multiple times
@@ -384,7 +391,7 @@ public void testGetId_multipleCalls_sameFIDReturned() throws Exception {
384391
.that(task2.getResult())
385392
.isEqualTo(TEST_FID_1);
386393
verify(mockBackend, times(1))
387-
.createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1);
394+
.createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1, null);
388395
PersistedInstallationEntry entry = persistedInstallation.readPersistedInstallationEntryValue();
389396
assertThat(entry.getFirebaseInstallationId(), equalTo(TEST_FID_1));
390397
assertTrue("the entry isn't doesn't have a registered fid: " + entry, entry.isRegistered());
@@ -399,7 +406,8 @@ public void testGetId_unregistered_replacesFidWithResponse() throws Exception {
399406
// Update local storage with installation entry that has invalid fid.
400407
persistedInstallation.insertOrUpdatePersistedInstallationEntry(
401408
PersistedInstallationEntry.INSTANCE.withUnregisteredFid("tobereplaced"));
402-
when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
409+
when(mockBackend.createFirebaseInstallation(
410+
anyString(), anyString(), anyString(), anyString(), any()))
403411
.thenReturn(TEST_INSTALLATION_RESPONSE);
404412

405413
// The first call will return the existing FID, "tobereplaced"
@@ -435,7 +443,8 @@ public void testGetId_ServerError_UnregisteredFID() throws Exception {
435443
PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1));
436444

437445
// have the server return a server error for the registration
438-
when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
446+
when(mockBackend.createFirebaseInstallation(
447+
anyString(), anyString(), anyString(), anyString(), any()))
439448
.thenReturn(
440449
InstallationResponse.builder().setResponseCode(ResponseCode.BAD_CONFIG).build());
441450

@@ -469,7 +478,8 @@ public void testGetId_fidRegistrationUncheckedException_statusUpdated() throws E
469478
PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1));
470479

471480
// Mocking unchecked exception on FIS createFirebaseInstallation
472-
when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
481+
when(mockBackend.createFirebaseInstallation(
482+
anyString(), anyString(), anyString(), anyString(), any()))
473483
.thenThrow(new IOException());
474484

475485
TestOnCompleteListener<String> onCompleteListener = new TestOnCompleteListener<>();
@@ -569,14 +579,15 @@ public void testGetId_expiredAuthToken_refreshesAuthToken() throws Exception {
569579
.isEqualTo(TEST_AUTH_TOKEN_2);
570580

571581
verify(mockBackend, never())
572-
.createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1);
582+
.createFirebaseInstallation(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_APP_ID_1, null);
573583
verify(mockBackend, times(1))
574584
.generateAuthToken(TEST_API_KEY, TEST_FID_1, TEST_PROJECT_ID, TEST_REFRESH_TOKEN);
575585
}
576586

577587
@Test
578588
public void testGetAuthToken_fidDoesNotExist_successful() throws Exception {
579-
when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
589+
when(mockBackend.createFirebaseInstallation(
590+
anyString(), anyString(), anyString(), anyString(), any()))
580591
.thenReturn(TEST_INSTALLATION_RESPONSE);
581592

582593
TestOnCompleteListener<InstallationTokenResult> onCompleteListener =
@@ -650,7 +661,8 @@ public void testGetToken_unregisteredFid_fetchedNewTokenFromFIS() throws Excepti
650661
// calls getId to ensure FID registration and returns a valid auth token.
651662
persistedInstallation.insertOrUpdatePersistedInstallationEntry(
652663
PersistedInstallationEntry.INSTANCE.withUnregisteredFid(TEST_FID_1));
653-
when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
664+
when(mockBackend.createFirebaseInstallation(
665+
anyString(), anyString(), anyString(), anyString(), any()))
654666
.thenReturn(TEST_INSTALLATION_RESPONSE);
655667

656668
TestOnCompleteListener<InstallationTokenResult> onCompleteListener =
@@ -859,7 +871,8 @@ public void testDelete_registeredFID_successful() throws Exception {
859871
utils.currentTimeInSecs(),
860872
TEST_AUTH_TOKEN,
861873
TEST_TOKEN_EXPIRATION_TIMESTAMP));
862-
when(mockBackend.createFirebaseInstallation(anyString(), anyString(), anyString(), anyString()))
874+
when(mockBackend.createFirebaseInstallation(
875+
anyString(), anyString(), anyString(), anyString(), any()))
863876
.thenReturn(TEST_INSTALLATION_RESPONSE);
864877

865878
TestOnCompleteListener<Void> onCompleteListener = new TestOnCompleteListener<>();

0 commit comments

Comments
 (0)