Repository: cal-itp/benefits

Issue:
Improve checks in `oauth` `authorize` view
We ran into a case while deploying #862 where our `auth_provider` model is configured with a `scope`, but the response from the Auth server doesn't actually contain the scope (probably still being set up).

The `oauth` `authorize` function doesn't handle this case and raises an `AttributeError`
```
  File "/home/calitp/app/benefits/oauth/views.py", line 70, in authorize
    claim_flag = (userinfo.get(verifier_claim) if userinfo else "false").lower() == "true"
AttributeError: 'NoneType' object has no attribute 'lower'
```

Let's add a check and tests for this so that the logs for this are more helpful.


Discussion:

Resolving patch:
diff --git a/benefits/oauth/views.py b/benefits/oauth/views.py
--- a/benefits/oauth/views.py
+++ b/benefits/oauth/views.py
@@ -66,10 +66,15 @@ def authorize(request):
 
     if verifier_claim:
         userinfo = token.get("userinfo")
-        # the claim comes back in userinfo like { "claim": "True" | "False" }
-        claim_flag = (userinfo.get(verifier_claim) if userinfo else "false").lower() == "true"
-        # if userinfo contains our claim and the flag is true, store the *claim*
-        stored_claim = verifier_claim if claim_flag else None
+
+        if userinfo:
+            claim_value = userinfo.get(verifier_claim)
+            # the claim comes back in userinfo like { "claim": "True" | "False" }
+            if claim_value is None:
+                logger.warning(f"userinfo did not contain: {verifier_claim}")
+            elif claim_value.lower() == "true":
+                # if userinfo contains our claim and the flag is true, store the *claim*
+                stored_claim = verifier_claim
 
     session.update(request, oauth_token=id_token, oauth_claim=stored_claim)
 


Test patch:
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
@@ -121,7 +121,9 @@ def test_authorize_success_with_claim_false(
 
 @pytest.mark.django_db
 @pytest.mark.usefixtures("mocked_analytics_module")
-def test_authorize_success_without_claim(mocked_session_verifier_auth_required, mocked_oauth_create_client, app_request):
+def test_authorize_success_without_verifier_claim(
+    mocked_session_verifier_auth_required, mocked_oauth_create_client, app_request
+):
     verifier = mocked_session_verifier_auth_required.return_value
     verifier.auth_provider.claim = ""
     mocked_oauth_client = mocked_oauth_create_client.return_value
@@ -135,6 +137,31 @@ def test_authorize_success_without_claim(mocked_session_verifier_auth_required,
     assert result.url == reverse(ROUTE_CONFIRM)
 
 
+@pytest.mark.django_db
+@pytest.mark.usefixtures("mocked_analytics_module")
+@pytest.mark.parametrize(
+    "access_token_response",
+    [
+        {"id_token": "token"},  # no userinfo
+        {"id_token": "token", "userinfo": {"something_unexpected": "True"}},  # has userinfo, but not the expected claim
+    ],
+)
+def test_authorize_success_without_claim_in_response(
+    mocked_session_verifier_auth_required, mocked_oauth_create_client, app_request, access_token_response
+):
+    verifier = mocked_session_verifier_auth_required.return_value
+    verifier.auth_provider.claim = "claim"
+    mocked_oauth_client = mocked_oauth_create_client.return_value
+    mocked_oauth_client.authorize_access_token.return_value = access_token_response
+
+    result = authorize(app_request)
+
+    mocked_oauth_client.authorize_access_token.assert_called_with(app_request)
+    assert session.oauth_claim(app_request) is None
+    assert result.status_code == 302
+    assert result.url == reverse(ROUTE_CONFIRM)
+
+
 def test_cancel(mocked_analytics_module, app_request):
     unverified_route = reverse(ROUTE_UNVERIFIED)