Repository: oppia/oppia

Issue:
PlatformFeatureService.saveResults() needs a new source for session IDs
**TL;DR:** The GAE-specific `SACSID` and `dev_appserver_login` HTTP session cookies are deprecated. Consequently, [`PlatformFeatureService.saveResults()`](https://github.com/oppia/oppia/blob/50f54920f9d5864fdd8f8b43cc69afbcd87cc4ef/core/templates/services/platform-feature.service.ts#L186-L199) must find a new source for session IDs.

---

Currently, `PlatformFeatureService` uses GAE-specific cookies to save "session-persistent" changes.

https://github.com/oppia/oppia/blob/50f54920f9d5864fdd8f8b43cc69afbcd87cc4ef/core/templates/services/platform-feature.service.ts#L186-L199

https://github.com/oppia/oppia/blob/50f54920f9d5864fdd8f8b43cc69afbcd87cc4ef/core/templates/services/platform-feature.service.ts#L280-L299

https://github.com/oppia/oppia/blob/50f54920f9d5864fdd8f8b43cc69afbcd87cc4ef/core/templates/services/platform-feature.service.ts#L67-L68

Both of these cookies are deprecated post-Firebase (#12172), and can no longer be used.

Note that we can't simply replace this with Firebase cookies, because those are unique to every user and would not have the desired effect (persistent even across different sign ins).


Discussion:
@brianrodri I think making the cookies tied to a specific user would be fine. The purpose of this caching check is to ensure that the platform parameters are reloaded when logging into a new user.

Would it be easy to hook into the new Firebase cookies? If not, I think disabling the tests to unblock any blocking PRs would be fine, but we shouldn't leave this broken for long.
@BenHenning Unfortunately not, session cookies are unique per-user _and_ per-session, so the same cookie value would not apply. I'm going to opt to disable the test and leave behind TODOs to find an alternative.

Out of curiosity, why wouldn't user ID work?
How would we handle this for non logged in users? @brianrodri 
Also, can we hide the platform parameter interface with a todo on the issue to reenable?
It doesn't seem like this worked for non logged users to begin with though, since a session only seems to correspond to an individual user, right? I'm not really familiar enough with the code or its intention to advise on a fix, I just know that we can no longer rely on SACSID or dev_appserver_login.
Hi @oppia/core-maintainers, this issue is not assigned to any project. Can you please update the same? Thanks!
From discussing with Brian during a meeting today, we realized that Firebase session cookie should be a suitable drop-in replacement for SACSID (which was acting as a GAE auth session ID before). This might actually be a better implementation since the session cookie is tied to the long-lived login status of an account (vs. a potentially arbitrary session since it wasn't clear the lifecycle of the GAE session ID).

Resolving patch:
diff --git a/core/feconf.py b/core/feconf.py
--- a/core/feconf.py
+++ b/core/feconf.py
@@ -1249,10 +1249,6 @@ def get_empty_ratings() -> Dict[str, int]:
 FIREBASE_EMULATOR_CONFIG_PATH = '.firebase.json'
 FIREBASE_EMULATOR_PORT = 9099
 
-# The name of the cookie Oppia will place the session cookie into. The name is
-# arbitrary. If it is changed later on, then the cookie will live on in the
-# users' browsers as garbage (although it would expire eventually, see MAX_AGE).
-FIREBASE_SESSION_COOKIE_NAME = 'session'
 # The duration a session cookie from Firebase should remain valid for. After the
 # duration expires, a new cookie will need to be generated. Generating a new
 # cookie requires the user to sign-in _explicitly_.
diff --git a/core/platform/auth/firebase_auth_services.py b/core/platform/auth/firebase_auth_services.py
--- a/core/platform/auth/firebase_auth_services.py
+++ b/core/platform/auth/firebase_auth_services.py
@@ -125,7 +125,7 @@ def establish_auth_session(
         _get_id_token(request), feconf.FIREBASE_SESSION_COOKIE_MAX_AGE)
 
     response.set_cookie(
-        feconf.FIREBASE_SESSION_COOKIE_NAME,
+        constants.FIREBASE_AUTH_SESSION_COOKIE_NAME,
         value=fresh_cookie,
         max_age=feconf.FIREBASE_SESSION_COOKIE_MAX_AGE,
         overwrite=True,
@@ -145,7 +145,7 @@ def destroy_auth_session(response: webapp2.Response) -> None:
     Args:
         response: webapp2.Response. Response to clear the cookies from.
     """
-    response.delete_cookie(feconf.FIREBASE_SESSION_COOKIE_NAME)
+    response.delete_cookie(constants.FIREBASE_AUTH_SESSION_COOKIE_NAME)
 
 
 def get_auth_claims_from_request(
@@ -507,7 +507,7 @@ def _get_session_cookie(request: webapp2.Request) -> Optional[str]:
         str|None. Value of the session cookie authorizing the signed in user, if
         present, otherwise None.
     """
-    return request.cookies.get(feconf.FIREBASE_SESSION_COOKIE_NAME)
+    return request.cookies.get(constants.FIREBASE_AUTH_SESSION_COOKIE_NAME)
 
 
 def _get_id_token(request: webapp2.Request) -> Optional[str]:


Test patch:
diff --git a/core/platform/auth/firebase_auth_services_test.py b/core/platform/auth/firebase_auth_services_test.py
--- a/core/platform/auth/firebase_auth_services_test.py
+++ b/core/platform/auth/firebase_auth_services_test.py
@@ -29,6 +29,7 @@
 from core import feconf
 from core import python_utils
 from core import utils
+from core.constants import constants
 from core.domain import auth_domain
 from core.domain import user_services
 from core.platform import models
@@ -950,7 +951,8 @@ def create_request(
         if id_token:
             req.headers['Authorization'] = 'Bearer %s' % id_token
         if session_cookie:
-            req.cookies[feconf.FIREBASE_SESSION_COOKIE_NAME] = session_cookie
+            req.cookies[constants.FIREBASE_AUTH_SESSION_COOKIE_NAME] = (
+                session_cookie)
         return req
 
     def create_response(
@@ -968,7 +970,8 @@ def create_response(
         res = webapp2.Response()
         if session_cookie:
             res.set_cookie(
-                feconf.FIREBASE_SESSION_COOKIE_NAME, value=session_cookie)
+                constants.FIREBASE_AUTH_SESSION_COOKIE_NAME,
+                value=session_cookie)
         return res
 
 
diff --git a/core/tests/protractor/featureGatingFlow.js b/core/tests/protractor/featureGatingFlow.js
--- a/core/tests/protractor/featureGatingFlow.js
+++ b/core/tests/protractor/featureGatingFlow.js
@@ -21,12 +21,22 @@ var general = require('../protractor_utils/general.js');
 var users = require('../protractor_utils/users.js');
 
 describe('Feature Gating Flow', function() {
+  var ADMIN_USER1_EMAIL = 'admin1@featureGatingFlow.com';
+  var ADMIN_USERNAME1 = 'featuregating1';
+  var ADMIN_USER2_EMAIL = 'admin2@featureGatingFlow.com';
+  var ADMIN_USERNAME2 = 'featuregating2';
+
   // Indicator in Angular component that is visible if the dummy_feature
   // is enabled, and the feature status is successfully loaded in the
   // Angular component.
   var agDummyFeatureIndicator = element(
     by.css('.protractor-test-angular-dummy-feature-indicator'));
 
+  // Indicator in Angular component that is visible if the dummy_feature
+  // is enabled, and the backend dummy handler is also enabled.
+  var agDummyHandlerIndicator = agDummyFeatureIndicator.element(
+    by.css('.protractor-test-angular-dummy-handler-indicator'));
+
   // Indicator in AngularJS directive that is visible if the dummy_feature
   // is enabled, and the feature status is successfully loaded in the
   // AngularJS directive.
@@ -37,6 +47,14 @@ describe('Feature Gating Flow', function() {
 
   beforeAll(async function() {
     adminPage = new AdminPage.AdminPage();
+
+    await users.createAndLoginCurriculumAdminUser(
+      ADMIN_USER1_EMAIL, ADMIN_USERNAME1);
+    await users.logout();
+
+    await users.createAndLoginCurriculumAdminUser(
+      ADMIN_USER2_EMAIL, ADMIN_USERNAME2);
+    await users.logout();
   });
 
   afterEach(async function() {
@@ -44,34 +62,29 @@ describe('Feature Gating Flow', function() {
   });
 
   afterAll(async function() {
-    await users.createAndLoginCurriculumAdminUser(
-      'admin1@featureGatingFlow.com', 'featuregating1');
+    await users.login(ADMIN_USER1_EMAIL, true);
 
     await adminPage.getFeaturesTab();
     var dummy = await adminPage.getDummyFeatureElement();
-    await adminPage.enableFeatureForDev(dummy);
+
     await adminPage.removeAllRulesOfFeature(dummy);
     await adminPage.saveChangeOfFeature(dummy);
-
     await users.logout();
   });
 
   it('should not show indicators gated by dummy feature when disabled',
     async() => {
-      await users.createAndLoginCurriculumAdminUser(
-        'admin2@featureGatingFlow.com', 'featuregating2');
-
+      await users.login(ADMIN_USER1_EMAIL, true);
       await adminPage.getFeaturesTab();
+
       expect(await agDummyFeatureIndicator.isPresent()).toBe(false);
       expect(await ajsDummyFeatureIndicator.isPresent()).toBe(false);
-
       await users.logout();
     }
   );
 
   it('should show dummy feature in the features tab', async() => {
-    await users.createAndLoginCurriculumAdminUser(
-      'admin3@featureGatingFlow.com', 'featuregating3');
+    await users.login(ADMIN_USER1_EMAIL, true);
 
     await adminPage.getFeaturesTab();
 
@@ -80,4 +93,23 @@ describe('Feature Gating Flow', function() {
     expect(await dummy.isPresent()).toBe(true);
     await users.logout();
   });
+
+  it('should not show indicators for dummy_feature to different users',
+    async() => {
+      await users.login(ADMIN_USER1_EMAIL, true);
+
+      await adminPage.getFeaturesTab();
+      var dummy = await adminPage.getDummyFeatureElement();
+      await adminPage.enableFeatureForDev(dummy);
+
+      await users.logout();
+      await users.login(ADMIN_USER2_EMAIL, true);
+
+      await adminPage.getFeaturesTab();
+
+      expect(await agDummyFeatureIndicator.isPresent()).toBe(false);
+      expect(await agDummyHandlerIndicator.isPresent()).toBe(false);
+      expect(await ajsDummyFeatureIndicator.isPresent()).toBe(false);
+      await users.logout();
+    });
 });