Repository: cal-itp/benefits

Issue:
Clear OAuth token on eligibility/index
Upon launching the Veterans flow, we'll have 2 separate flows that use the OAuth infrastructure.

Because of the way the IdG interacts with VA.gov, we can't sign the user out all the way up the stack to VA.gov's identity provider. We'll deal with this from a UI perspective in #1456.

However for a user that e.g. fails verification in one flow and would like to try the other, we need to clear the app's local OAuth token, which is provided by the IdG, in between those attempts -- otherwise the app will the think the user has already signed in with IdG, and will re-use the old token with incorrect/missing claim(s).

Proposal is to reset the token on all loads of the `eligibility/index` view. This will change the behavior for successful verifications, in that users who return to the `eligibility/index` view after having been verified currently can pass-through verification to enrollment. After this ticket is implemented, users will always have to click to sign-in again (but may be able to skip the full Login.gov sign-in process as they would not be fully signed-out from there unless they clicked the "Sign out" button in the app). 

See Slack conversation for more context: https://cal-itp.slack.com/archives/C037Y3UE71P/p1687889544724039


Discussion:

Resolving patch:
diff --git a/benefits/core/session.py b/benefits/core/session.py
--- a/benefits/core/session.py
+++ b/benefits/core/session.py
@@ -147,8 +147,8 @@ def logged_in(request):
 
 
 def logout(request):
-    """Reset the session tokens."""
-    update(request, oauth_token=False, enrollment_token=False)
+    """Reset the session claims and tokens."""
+    update(request, oauth_claim=False, oauth_token=False, enrollment_token=False)
 
 
 def oauth_token(request):
diff --git a/benefits/eligibility/views.py b/benefits/eligibility/views.py
--- a/benefits/eligibility/views.py
+++ b/benefits/eligibility/views.py
@@ -46,6 +46,10 @@ def index(request, agency=None):
     else:
         session.update(request, agency=agency, eligibility_types=[], origin=reverse(ROUTE_CORE_INDEX))
 
+    # clear any prior OAuth token as the user is choosing their desired flow
+    # this may or may not require OAuth, with a different set of scope/claims than what is already stored
+    session.logout(request)
+
     eligibility_start = reverse(ROUTE_START)
 
     help_page = reverse(ROUTE_HELP)
diff --git a/benefits/oauth/client.py b/benefits/oauth/client.py
--- a/benefits/oauth/client.py
+++ b/benefits/oauth/client.py
@@ -21,7 +21,7 @@ def _client_kwargs(scope=None):
     `scope` should be a space-separated list of scopes to add.
     """
     scopes = ["openid", scope] if scope else ["openid"]
-    return {"code_challenge_method": "S256", "scope": " ".join(scopes)}
+    return {"code_challenge_method": "S256", "scope": " ".join(scopes), "prompt": "login"}
 
 
 def _server_metadata_url(authority):


Test patch:
diff --git a/tests/pytest/core/test_session.py b/tests/pytest/core/test_session.py
--- a/tests/pytest/core/test_session.py
+++ b/tests/pytest/core/test_session.py
@@ -144,14 +144,16 @@ def test_logged_in_True(app_request):
 
 @pytest.mark.django_db
 def test_logout(app_request):
-    session.update(app_request, oauth_token="oauth_token", enrollment_token="enrollment_token")
+    session.update(app_request, oauth_claim="oauth_claim", oauth_token="oauth_token", enrollment_token="enrollment_token")
     assert session.logged_in(app_request)
+    assert session.oauth_claim(app_request)
 
     session.logout(app_request)
 
     assert not session.logged_in(app_request)
     assert not session.enrollment_token(app_request)
     assert not session.oauth_token(app_request)
+    assert not session.oauth_claim(app_request)
 
 
 @pytest.mark.django_db
diff --git a/tests/pytest/eligibility/test_views.py b/tests/pytest/eligibility/test_views.py
--- a/tests/pytest/eligibility/test_views.py
+++ b/tests/pytest/eligibility/test_views.py
@@ -2,8 +2,8 @@
 
 import pytest
 
-
 from benefits.core.middleware import TEMPLATE_USER_ERROR
+import benefits.core.session
 from benefits.eligibility.forms import EligibilityVerifierSelectionForm
 from benefits.eligibility.views import (
     ROUTE_INDEX,
@@ -44,6 +44,11 @@ def mocked_eligibility_auth_request(mocked_eligibility_request_session, mocked_s
     pass
 
 
+@pytest.fixture
+def session_logout_spy(mocker):
+    return mocker.spy(benefits.core.session, "logout")
+
+
 @pytest.fixture
 def mocked_verifier_form(mocker):
     mock_form = mocker.Mock(spec=EligibilityVerifierSelectionForm)
@@ -126,6 +131,16 @@ def test_index_post_valid_form(client, model_EligibilityVerifier, mocked_session
     mocked_analytics_module.selected_verifier.assert_called_once()
 
 
+@pytest.mark.django_db
+@pytest.mark.usefixtures("mocked_eligibility_auth_request")
+def test_index_calls_session_logout(client, session_logout_spy):
+    path = reverse(ROUTE_INDEX)
+
+    client.get(path)
+
+    session_logout_spy.assert_called_once()
+
+
 @pytest.mark.django_db
 @pytest.mark.usefixtures("mocked_session_agency", "mocked_verifier_form", "mocked_session_verifier_auth_required")
 def test_start_verifier_auth_required_logged_in(mocker, client):
diff --git a/tests/pytest/oauth/test_client.py b/tests/pytest/oauth/test_client.py
--- a/tests/pytest/oauth/test_client.py
+++ b/tests/pytest/oauth/test_client.py
@@ -8,13 +8,13 @@ def test_client_kwargs():
     kwargs = _client_kwargs()
 
     assert kwargs["code_challenge_method"] == "S256"
+    assert kwargs["prompt"] == "login"
     assert "openid" in kwargs["scope"]
 
 
 def test_client_kwargs_scope():
     kwargs = _client_kwargs("scope1")
 
-    assert kwargs["code_challenge_method"] == "S256"
     assert "openid" in kwargs["scope"]
     assert "scope1" in kwargs["scope"]
 
diff --git a/tests/pytest/oauth/test_views.py b/tests/pytest/oauth/test_views.py
--- a/tests/pytest/oauth/test_views.py
+++ b/tests/pytest/oauth/test_views.py
@@ -201,8 +201,11 @@ def test_logout(mocker, mocked_oauth_create_client, mocked_analytics_module, app
     mocked_analytics_module.started_sign_out.assert_called_once()
     assert result.status_code == 200
     assert message in str(result.content)
+
     assert not session.logged_in(app_request)
     assert session.enrollment_token(app_request) is False
+    assert session.oauth_token(app_request) is False
+    assert session.oauth_claim(app_request) is False
 
 
 def test_post_logout(app_request, mocked_analytics_module):