From 8ff746f1438d0ae1df7bc5d440df9913827239f3 Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Wed, 20 Nov 2024 13:17:39 -0500 Subject: [PATCH 01/23] Add name suffix to TrueID response fixture (#11534) LexisNexis informed us that the name suffix should be available in a TrueID response if the name suffix is present on the document. This commit adds the name suffix to the response fixture so that we have a fixture to represent this case. We plan to read the name suffix and check it against the DLDV service in a future change. This commit prepares us for that. [skip changelog] --- .../lexis_nexis/true_id/true_id_response_success.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_success.json b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_success.json index 770b875a008..95f7606fa14 100644 --- a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_success.json +++ b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_success.json @@ -328,6 +328,11 @@ "Group": "IDAUTH_FIELD_DATA", "Name": "Fields_FirstName", "Values": [{"Value": "LICENSE"}] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_NameSuffix", + "Values": [{"Value": "JR"}] }, { "Group": "IDAUTH_FIELD_DATA", From 108f2aea8afdc402af06ba1faad05d186f108f41 Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Wed, 20 Nov 2024 13:49:01 -0500 Subject: [PATCH 02/23] Fix the whitespace in the TrueID success response fixtures (#11535) In #11534 we identified that this file was not properly formatted. This commit updates the contents of the file to be formatted using `jq`. [skip changelog] --- .../true_id/true_id_response_success.json | 1276 +++++++++++------ 1 file changed, 804 insertions(+), 472 deletions(-) diff --git a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_success.json b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_success.json index 95f7606fa14..6b171049751 100644 --- a/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_success.json +++ b/spec/fixtures/proofing/lexis_nexis/true_id/true_id_response_success.json @@ -1,479 +1,811 @@ { - "Status": { - "ConversationId": "31000403205968", - "RequestId": "705004858", - "TransactionStatus": "passed", - "Reference": "Reference1", - "ServerInfo": "bctlsidmapp01.risk.regn.net" - }, - "Products": [ { + "Status": { + "ConversationId": "31000403205968", + "RequestId": "705004858", + "TransactionStatus": "passed", + "Reference": "Reference1", + "ServerInfo": "bctlsidmapp01.risk.regn.net" + }, + "Products": [ + { "ProductType": "TrueID", "ExecutedStepName": "True_ID_Step", "ProductConfigurationName": "AndreV3_TrueID_Flow", "ProductStatus": "pass", - "ParameterDetails": [ - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocumentName", - "Values": [{"Value": "New York (NY) Learner Permit"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocAuthResult", - "Values": [{"Value": "Passed"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocIssuerCode", - "Values": [{"Value": "NY"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocIssuerName", - "Values": [{"Value": "New York"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocClassCode", - "Values": [{"Value": "DriversLicense"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocClass", - "Values": [{"Value": "DriversLicense"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocClassName", - "Values": [{"Value": "Drivers License"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocIsGeneric", - "Values": [{"Value": "false"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocIssue", - "Values": [{"Value": "1997"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocIssueType", - "Values": [{"Value": "Learner Permit"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DocSize", - "Values": [{"Value": "ID1"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "ClassificationMode", - "Values": [{"Value": "Automatic"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "OrientationChanged", - "Values": [{"Value": "false"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "PresentationChanged", - "Values": [{"Value": "false"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "Side", - "Values": [{"Value": "Front"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "GlareMetric", - "Values": [{"Value": "95"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "SharpnessMetric", - "Values": [{"Value": "64"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "IsTampered", - "Values": [{"Value": "0"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "IsCropped", - "Values": [{"Value": "0"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "HorizontalResolution", - "Values": [{"Value": "353"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "VerticalResolution", - "Values": [{"Value": "353"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "Light", - "Values": [{"Value": "White"}] - }, - { - "Group": "IMAGE_METRICS_RESULT", - "Name": "MimeType", - "Values": [{"Value": "image/jpeg"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "FullName", - "Values": [{"Value": "LICENSE SAMPLE"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Sex", - "Values": [{"Value": "Male"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Age", - "Values": [{"Value": "54"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DOB_Year", - "Values": [{"Value": "1966"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DOB_Month", - "Values": [{"Value": "5"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "DOB_Day", - "Values": [{"Value": "5"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "ExpirationDate_Year", - "Values": [{"Value": "2099"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "ExpirationDate_Month", - "Values": [{"Value": "5"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "ExpirationDate_Day", - "Values": [{"Value": "5"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Portrait", - "Values": [{"Value": "/9j/4AAQSkZJRgABAQEBYgFiAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9\nPDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhC\nY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAAR\nCAGzAVwDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA\nAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK\nFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG\nh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl\n5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA\nAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk\nNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE\nhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk\n5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDSkZiHQszc5AJqATOSshYgjg805xmNHP8A\nD1qEEBzxkNSNCRZNpPzHD56GlDMVwzHI9TUOxijKv3kPHNPZhgPjrwaYD/OUDeGbnjigONrR\nEkhvunNMUYZlJ425FNBzEMclDyfWkA8yOY8ZO9MZwaeZWVwckq/HFR7trA9QwximAHa8TcHO\nQaAJA+dyMSWTkc07ziqpJkkdDioiS3ly9ME7qRh8zKDwwz7UwJAcKU3/AOsHHtSecSFY53J7\n1HnCMO68ilLZPUBWHNAEjS993DDoTTd+QYyenI5poQBNpJODkGkMgPzDqODQBKZG2I+9iBwR\nmoiyqxBPyyDP0pxJjO3++MjPas66ncBk3jco5oE2WmuVEBffyjcfSo2v8ndENwdeTmsSa4aR\n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\n/Hv/AMCNSZJmTPpRRTAbH92WpIf9Sv0oooQM/9k="}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_1_AlertName", - "Values": [{"Value": "Birth Date Crosscheck"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_2_AlertName", - "Values": [{"Value": "Visible Pattern"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_3_AlertName", - "Values": [{"Value": "Birth Date Valid"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_4_AlertName", - "Values": [{"Value": "Document Classification"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_5_AlertName", - "Values": [{"Value": "Document Expired"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_6_AlertName", - "Values": [{"Value": "Expiration Date Valid"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_7_AlertName", - "Values": [{"Value": "Issue Date Valid"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_8_AlertName", - "Values": [{"Value": "Visible Pattern"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_9_AlertName", - "Values": [{"Value": "Visible Pattern"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_1_AuthenticationResult", - "Values": [ { - "Value": "Failed", - "Detail": "Compare the machine-readable birth date field to the human-readable birth date field." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_2_AuthenticationResult", - "Values": [ { - "Value": "Failed", - "Detail": "Verified the presence of a pattern on the visible image." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_3_AuthenticationResult", - "Values": [ { - "Value": "Passed", - "Detail": "Verified that the birth date is valid." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_4_AuthenticationResult", - "Values": [ { - "Value": "Passed", - "Detail": "Verified that the type of document is supported and is able to be fully authenticated." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_5_AuthenticationResult", - "Values": [ { - "Value": "Passed", - "Detail": "Checked if the document is expired." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_6_AuthenticationResult", - "Values": [ { - "Value": "Passed", - "Detail": "Verified that the expiration date is valid." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_7_AuthenticationResult", - "Values": [ { - "Value": "Passed", - "Detail": "Verified that the issue date is valid." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_8_AuthenticationResult", - "Values": [ { - "Value": "Passed", - "Detail": "Verified the presence of a pattern on the visible image." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_9_AuthenticationResult", - "Values": [ { - "Value": "Passed", - "Detail": "Verified the presence of a pattern on the visible image." - }] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_2_Regions", - "Values": [{"Value": "Verify Layout 1"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_8_Regions", - "Values": [{"Value": "Visible Pattern"}] - }, - { - "Group": "AUTHENTICATION_RESULT", - "Name": "Alert_9_Regions", - "Values": [{"Value": "Name Registration Verify"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_FullName", - "Values": [{"Value": "LICENSE SAMPLE"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_Surname", - "Values": [{"Value": "SAMPLE"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_GivenName", - "Values": [{"Value": "LICENSE"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_FirstName", - "Values": [{"Value": "LICENSE"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_NameSuffix", - "Values": [{"Value": "JR"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_DOB_Year", - "Values": [{"Value": "1966"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_DOB_Month", - "Values": [{"Value": "5"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_DOB_Day", - "Values": [{"Value": "5"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_DocumentClassName", - "Values": [{"Value": "Drivers License"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_DocumentNumber", - "Values": [{"Value": "020000060"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_ExpirationDate_Year", - "Values": [{"Value": "2099"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_ExpirationDate_Month", - "Values": [{"Value": "5"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_xpirationDate_Day", - "Values": [{"Value": "5"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_IssuingStateCode", - "Values": [{"Value": "NY"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_IssuingStateName", - "Values": [{"Value": "New York"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_Address", - "Values": [{"Value": "123 ABC AVE
ANYTOWN NY
12345"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_AddressLine1", - "Values": [{"Value": "123 ABC AVE"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_AddressLine2", - "Values": [{"Value": "APT 3E"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_City", - "Values": [{"Value": "ANYTOWN"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_State", - "Values": [{"Value": "NY"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_PostalCode", - "Values": [{"Value": "12345"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_Sex", - "Values": [{"Value": "M"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_ControlNumber", - "Values": [{"Value": "6820051160"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_Height", - "Values": [{"Value": "5'08\""}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_IssueDate_Year", - "Values": [{"Value": "1997"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_IssueDate_Month", - "Values": [{"Value": "7"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_IssueDate_Day", - "Values": [{"Value": "15"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_LicenseClass", - "Values": [{"Value": "D"}] - }, - { - "Group": "IDAUTH_FIELD_DATA", - "Name": "Fields_LicenseRestrictions", - "Values": [{"Value": "B"}] - }, - { - "Group": "PORTRAIT_MATCH_RESULT", - "Name": "FaceMatchResult", - "Values": [{"Value": "Fail"}] - }, - { - "Group": "PORTRAIT_MATCH_RESULT", - "Name": "FaceMatchScore", - "Values": [{"Value": "0"}] - }, - { - "Group": "PORTRAIT_MATCH_RESULT", - "Name": "FaceStatusCode", - "Values": [{"Value": "0"}] - }, - { - "Group": "PORTRAIT_MATCH_RESULT", - "Name": "FaceErrorMessage", - "Values": [{"Value": "Liveness: PoorQuality"}] - } + "ParameterDetails": [ + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocumentName", + "Values": [ + { + "Value": "New York (NY) Learner Permit" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocAuthResult", + "Values": [ + { + "Value": "Passed" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssuerCode", + "Values": [ + { + "Value": "NY" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssuerName", + "Values": [ + { + "Value": "New York" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocClassCode", + "Values": [ + { + "Value": "DriversLicense" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocClass", + "Values": [ + { + "Value": "DriversLicense" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocClassName", + "Values": [ + { + "Value": "Drivers License" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIsGeneric", + "Values": [ + { + "Value": "false" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssue", + "Values": [ + { + "Value": "1997" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocIssueType", + "Values": [ + { + "Value": "Learner Permit" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DocSize", + "Values": [ + { + "Value": "ID1" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ClassificationMode", + "Values": [ + { + "Value": "Automatic" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "OrientationChanged", + "Values": [ + { + "Value": "false" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "PresentationChanged", + "Values": [ + { + "Value": "false" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "Side", + "Values": [ + { + "Value": "Front" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "GlareMetric", + "Values": [ + { + "Value": "95" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "SharpnessMetric", + "Values": [ + { + "Value": "64" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "IsTampered", + "Values": [ + { + "Value": "0" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "IsCropped", + "Values": [ + { + "Value": "0" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "HorizontalResolution", + "Values": [ + { + "Value": "353" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "VerticalResolution", + "Values": [ + { + "Value": "353" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "Light", + "Values": [ + { + "Value": "White" + } + ] + }, + { + "Group": "IMAGE_METRICS_RESULT", + "Name": "MimeType", + "Values": [ + { + "Value": "image/jpeg" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "FullName", + "Values": [ + { + "Value": "LICENSE SAMPLE" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Sex", + "Values": [ + { + "Value": "Male" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Age", + "Values": [ + { + "Value": "54" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DOB_Year", + "Values": [ + { + "Value": "1966" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DOB_Month", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "DOB_Day", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ExpirationDate_Year", + "Values": [ + { + "Value": "2099" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ExpirationDate_Month", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "ExpirationDate_Day", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Portrait", + "Values": [ + { + "Value": "/9j/4AAQSkZJRgABAQEBYgFiAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9\nPDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhC\nY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAAR\nCAGzAVwDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA\nAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkK\nFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWG\nh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl\n5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREA\nAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk\nNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOE\nhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk\n5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDSkZiHQszc5AJqATOSshYgjg805xmNHP8A\nD1qEEBzxkNSNCRZNpPzHD56GlDMVwzHI9TUOxijKv3kPHNPZhgPjrwaYD/OUDeGbnjigONrR\nEkhvunNMUYZlJ425FNBzEMclDyfWkA8yOY8ZO9MZwaeZWVwckq/HFR7trA9QwximAHa8TcHO\nQaAJA+dyMSWTkc07ziqpJkkdDioiS3ly9ME7qRh8zKDwwz7UwJAcKU3/AOsHHtSecSFY53J7\n1HnCMO68ilLZPUBWHNAEjS993DDoTTd+QYyenI5poQBNpJODkGkMgPzDqODQBKZG2I+9iBwR\nmoiyqxBPyyDP0pxJjO3++MjPas66ncBk3jco5oE2WmuVEBffyjcfSo2v8ndENwdeTmsSa4aR\n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\n/Hv/AMCNSZJmTPpRRTAbH92WpIf9Sv0oooQM/9k=" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_1_AlertName", + "Values": [ + { + "Value": "Birth Date Crosscheck" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_AlertName", + "Values": [ + { + "Value": "Visible Pattern" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_AlertName", + "Values": [ + { + "Value": "Birth Date Valid" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_4_AlertName", + "Values": [ + { + "Value": "Document Classification" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_5_AlertName", + "Values": [ + { + "Value": "Document Expired" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_6_AlertName", + "Values": [ + { + "Value": "Expiration Date Valid" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_7_AlertName", + "Values": [ + { + "Value": "Issue Date Valid" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_8_AlertName", + "Values": [ + { + "Value": "Visible Pattern" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_9_AlertName", + "Values": [ + { + "Value": "Visible Pattern" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_1_AuthenticationResult", + "Values": [ + { + "Value": "Failed", + "Detail": "Compare the machine-readable birth date field to the human-readable birth date field." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_AuthenticationResult", + "Values": [ + { + "Value": "Failed", + "Detail": "Verified the presence of a pattern on the visible image." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_3_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the birth date is valid." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_4_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the type of document is supported and is able to be fully authenticated." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_5_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Checked if the document is expired." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_6_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the expiration date is valid." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_7_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified that the issue date is valid." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_8_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified the presence of a pattern on the visible image." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_9_AuthenticationResult", + "Values": [ + { + "Value": "Passed", + "Detail": "Verified the presence of a pattern on the visible image." + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_2_Regions", + "Values": [ + { + "Value": "Verify Layout 1" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_8_Regions", + "Values": [ + { + "Value": "Visible Pattern" + } + ] + }, + { + "Group": "AUTHENTICATION_RESULT", + "Name": "Alert_9_Regions", + "Values": [ + { + "Value": "Name Registration Verify" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_FullName", + "Values": [ + { + "Value": "LICENSE SAMPLE" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_Surname", + "Values": [ + { + "Value": "SAMPLE" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_GivenName", + "Values": [ + { + "Value": "LICENSE" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_FirstName", + "Values": [ + { + "Value": "LICENSE" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_NameSuffix", + "Values": [ + { + "Value": "JR" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DOB_Year", + "Values": [ + { + "Value": "1966" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DOB_Month", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DOB_Day", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DocumentClassName", + "Values": [ + { + "Value": "Drivers License" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_DocumentNumber", + "Values": [ + { + "Value": "020000060" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_ExpirationDate_Year", + "Values": [ + { + "Value": "2099" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_ExpirationDate_Month", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_xpirationDate_Day", + "Values": [ + { + "Value": "5" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssuingStateCode", + "Values": [ + { + "Value": "NY" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssuingStateName", + "Values": [ + { + "Value": "New York" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_Address", + "Values": [ + { + "Value": "123 ABC AVE
ANYTOWN NY
12345" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_AddressLine1", + "Values": [ + { + "Value": "123 ABC AVE" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_AddressLine2", + "Values": [ + { + "Value": "APT 3E" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_City", + "Values": [ + { + "Value": "ANYTOWN" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_State", + "Values": [ + { + "Value": "NY" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_PostalCode", + "Values": [ + { + "Value": "12345" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_Sex", + "Values": [ + { + "Value": "M" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_ControlNumber", + "Values": [ + { + "Value": "6820051160" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_Height", + "Values": [ + { + "Value": "5'08\"" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssueDate_Year", + "Values": [ + { + "Value": "1997" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssueDate_Month", + "Values": [ + { + "Value": "7" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_IssueDate_Day", + "Values": [ + { + "Value": "15" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_LicenseClass", + "Values": [ + { + "Value": "D" + } + ] + }, + { + "Group": "IDAUTH_FIELD_DATA", + "Name": "Fields_LicenseRestrictions", + "Values": [ + { + "Value": "B" + } + ] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceMatchResult", + "Values": [ + { + "Value": "Fail" + } + ] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceMatchScore", + "Values": [ + { + "Value": "0" + } + ] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceStatusCode", + "Values": [ + { + "Value": "0" + } + ] + }, + { + "Group": "PORTRAIT_MATCH_RESULT", + "Name": "FaceErrorMessage", + "Values": [ + { + "Value": "Liveness: PoorQuality" + } + ] + } ] - }] + } + ] } From d31f490a7b38d4a3d9d876ce7a6c061b35715983 Mon Sep 17 00:00:00 2001 From: "Davida (she/they)" Date: Wed, 20 Nov 2024 13:54:39 -0500 Subject: [PATCH 03/23] Dmm/clean up config (#11533) * changelog: Internal, Facial Match, Clean up config post-GA --- app/models/service_provider.rb | 3 +- config/application.yml.default | 9 ---- lib/identity_config.rb | 5 --- lib/saml_idp_constants.rb | 3 +- .../idv/doc_auth/document_capture_spec.rb | 10 ----- .../idv/doc_auth/how_to_verify_spec.rb | 5 --- .../idv/doc_auth/hybrid_handoff_spec.rb | 5 --- .../doc_auth/redo_document_capture_spec.rb | 10 ----- .../idv/steps/in_person_opt_in_ipp_spec.rb | 3 -- .../reports/authorization_count_spec.rb | 3 -- .../openid_connect_authorize_form_spec.rb | 31 ++------------ spec/models/service_provider_spec.rb | 42 ++----------------- spec/services/saml_request_validator_spec.rb | 3 -- 13 files changed, 10 insertions(+), 122 deletions(-) diff --git a/app/models/service_provider.rb b/app/models/service_provider.rb index 34634361288..8146db65b21 100644 --- a/app/models/service_provider.rb +++ b/app/models/service_provider.rb @@ -79,8 +79,7 @@ def ialmax_allowed? end def facial_match_ial_allowed? - IdentityConfig.store.facial_match_general_availability_enabled || - IdentityConfig.store.allowed_biometric_ial_providers.include?(issuer) + IdentityConfig.store.facial_match_general_availability_enabled end private diff --git a/config/application.yml.default b/config/application.yml.default index a69b70d25ea..2cb0f53eaf1 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -34,9 +34,7 @@ acuant_sdk_initialization_endpoint: 'https://us.acas.acuant.net' add_email_link_valid_for_hours: 24 address_identity_proofing_supported_country_codes: '["AS", "GU", "MP", "PR", "US", "VI"]' all_redirect_uris_cache_duration_minutes: 2 -allowed_biometric_ial_providers: '[]' allowed_ialmax_providers: '[]' -allowed_valid_authn_contexts_semantic_providers: '[]' allowed_verified_within_providers: '[]' asset_host: '' async_stale_job_timeout_seconds: 300 @@ -59,7 +57,6 @@ backup_code_user_id_per_ip_attempt_window_exponential_factor: 1.1 backup_code_user_id_per_ip_attempt_window_in_minutes: 720 backup_code_user_id_per_ip_attempt_window_max_minutes: 43_200 backup_code_user_id_per_ip_max_attempts: 50 -biometric_ial_enabled: true broken_personal_key_window_finish: '2021-09-22T00:00:00Z' broken_personal_key_window_start: '2021-07-29T00:00:00Z' check_user_password_compromised_enabled: false @@ -133,7 +130,6 @@ facial_match_general_availability_enabled: true feature_idv_force_gpo_verification_enabled: false feature_idv_hybrid_flow_enabled: true feature_select_email_to_share_enabled: true -feature_valid_authn_contexts_semantic_enabled: true geo_data_file_path: 'geo_data/GeoLite2-City.mmdb' get_usps_proofing_results_job_cron: '0/30 * * * *' get_usps_proofing_results_job_reprocess_delay_minutes: 5 @@ -426,7 +422,6 @@ usps_upload_sftp_password: '' usps_upload_sftp_timeout: 5 usps_upload_sftp_username: '' valid_authn_contexts: '["http://idmanagement.gov/ns/assurance/loa/1", "http://idmanagement.gov/ns/assurance/loa/3", "http://idmanagement.gov/ns/assurance/ial/1", "http://idmanagement.gov/ns/assurance/ial/2", "http://idmanagement.gov/ns/assurance/ial/0", "http://idmanagement.gov/ns/assurance/ial/2?strict=true", "http://idmanagement.gov/ns/assurance/ial/2?bio=preferred", "http://idmanagement.gov/ns/assurance/ial/2?bio=required", "urn:gov:gsa:ac:classes:sp:PasswordProtectedTransport:duo", "http://idmanagement.gov/ns/assurance/aal/2", "http://idmanagement.gov/ns/assurance/aal/3", "http://idmanagement.gov/ns/assurance/aal/3?hspd12=true","http://idmanagement.gov/ns/assurance/aal/2?phishing_resistant=true","http://idmanagement.gov/ns/assurance/aal/2?hspd12=true", "urn:acr.login.gov:auth-only", "urn:acr.login.gov:verified","urn:acr.login.gov:verified-facial-match-preferred","urn:acr.login.gov:verified-facial-match-required"]' -valid_authn_contexts_semantic: '["http://idmanagement.gov/ns/assurance/loa/1", "http://idmanagement.gov/ns/assurance/loa/3", "http://idmanagement.gov/ns/assurance/ial/1", "http://idmanagement.gov/ns/assurance/ial/2", "http://idmanagement.gov/ns/assurance/ial/0", "http://idmanagement.gov/ns/assurance/ial/2?strict=true", "http://idmanagement.gov/ns/assurance/ial/2?bio=preferred", "http://idmanagement.gov/ns/assurance/ial/2?bio=required", "urn:gov:gsa:ac:classes:sp:PasswordProtectedTransport:duo", "http://idmanagement.gov/ns/assurance/aal/2", "http://idmanagement.gov/ns/assurance/aal/3", "http://idmanagement.gov/ns/assurance/aal/3?hspd12=true","http://idmanagement.gov/ns/assurance/aal/2?phishing_resistant=true","http://idmanagement.gov/ns/assurance/aal/2?hspd12=true", "urn:acr.login.gov:auth-only", "urn:acr.login.gov:verified","urn:acr.login.gov:verified-facial-match-preferred","urn:acr.login.gov:verified-facial-match-required"]' vendor_status_idv_scheduled_maintenance_finish: '' vendor_status_idv_scheduled_maintenance_start: '' vendor_status_lexisnexis_instant_verify: 'operational' @@ -507,7 +502,6 @@ production: aamva_send_id_type: false aamva_verification_url: 'https://verificationservices-cert.aamva.org:18449/dldv/2.1/online' available_locales: 'en,es,fr' - biometric_ial_enabled: false disable_email_sending: false disable_logout_get_request: false email_registrations_per_ip_track_only_mode: true @@ -515,7 +509,6 @@ production: enable_usps_verification: false facial_match_general_availability_enabled: false feature_select_email_to_share_enabled: false - feature_valid_authn_contexts_semantic_enabled: false idv_sp_required: true invalid_gpo_confirmation_zipcode: '' lexisnexis_threatmetrix_mock_enabled: false @@ -542,8 +535,6 @@ test: aamva_private_key: 123abc aamva_public_key: 123abc account_reset_fraud_user_wait_period_days: 30 - allowed_biometric_ial_providers: '["urn:gov:gsa:openidconnect:sp:server"]' - allowed_valid_authn_contexts_semantic_providers: '["urn:gov:gsa:openidconnect:sp:server"]' attribute_encryption_key: 2086dfbd15f5b0c584f3664422a1d3409a0d2aa6084f65b6ba57d64d4257431c124158670c7655e45cabe64194f7f7b6c7970153c285bdb8287ec0c4f7553e25 attribute_encryption_key_queue: '[{ "key": "11111111111111111111111111111111" }, { "key": "22222222222222222222222222222222" }]' dashboard_api_token: 123ABC diff --git a/lib/identity_config.rb b/lib/identity_config.rb index ea6d23bfc8d..35957cc6a58 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -52,9 +52,7 @@ def self.store config.add(:add_email_link_valid_for_hours, type: :integer) config.add(:address_identity_proofing_supported_country_codes, type: :json) config.add(:all_redirect_uris_cache_duration_minutes, type: :integer) - config.add(:allowed_biometric_ial_providers, type: :json) config.add(:allowed_ialmax_providers, type: :json) - config.add(:allowed_valid_authn_contexts_semantic_providers, type: :json) config.add(:allowed_verified_within_providers, type: :json) config.add(:asset_host, type: :string) config.add(:async_stale_job_timeout_seconds, type: :integer) @@ -77,7 +75,6 @@ def self.store config.add(:backup_code_user_id_per_ip_attempt_window_in_minutes, type: :integer) config.add(:backup_code_user_id_per_ip_attempt_window_max_minutes, type: :integer) config.add(:backup_code_user_id_per_ip_max_attempts, type: :integer) - config.add(:biometric_ial_enabled, type: :boolean) config.add(:broken_personal_key_window_finish, type: :timestamp) config.add(:broken_personal_key_window_start, type: :timestamp) config.add(:check_user_password_compromised_enabled, type: :boolean) @@ -151,7 +148,6 @@ def self.store config.add(:feature_idv_force_gpo_verification_enabled, type: :boolean) config.add(:feature_idv_hybrid_flow_enabled, type: :boolean) config.add(:feature_select_email_to_share_enabled, type: :boolean) - config.add(:feature_valid_authn_contexts_semantic_enabled, type: :boolean) config.add(:geo_data_file_path, type: :string) config.add(:get_usps_proofing_results_job_cron, type: :string) config.add(:get_usps_proofing_results_job_reprocess_delay_minutes, type: :integer) @@ -458,7 +454,6 @@ def self.store config.add(:usps_upload_sftp_timeout, type: :integer) config.add(:usps_upload_sftp_username, type: :string) config.add(:valid_authn_contexts, type: :json) - config.add(:valid_authn_contexts_semantic, type: :json) config.add(:vendor_status_lexisnexis_instant_verify, type: :symbol, enum: VENDOR_STATUS_OPTIONS) config.add(:vendor_status_lexisnexis_phone_finder, type: :symbol, enum: VENDOR_STATUS_OPTIONS) config.add(:vendor_status_lexisnexis_trueid, type: :symbol, enum: VENDOR_STATUS_OPTIONS) diff --git a/lib/saml_idp_constants.rb b/lib/saml_idp_constants.rb index ac117a62739..240e9373253 100644 --- a/lib/saml_idp_constants.rb +++ b/lib/saml_idp_constants.rb @@ -47,8 +47,7 @@ module Constants REQUESTED_ATTRIBUTES_CLASSREF = "#{LEGACY_ACR_NS}/requested_attributes?ReqAttr=".freeze - # TODO: replace valid_authn_contexts_semantic with valid_authn_contexts - VALID_AUTHN_CONTEXTS = IdentityConfig.store.valid_authn_contexts_semantic.freeze + VALID_AUTHN_CONTEXTS = IdentityConfig.store.valid_authn_contexts.freeze FACIAL_MATCH_IAL_CONTEXTS = [ IAL_VERIFIED_FACIAL_MATCH_REQUIRED_ACR, diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index dd1f0adb23a..3d6c44d8632 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -502,11 +502,6 @@ :in_person_proofing_enabled, ).and_return(true) allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(99) - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([ipp_service_provider.issuer]) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([ipp_service_provider.issuer]) perform_in_browser(:mobile) do visit_idp_from_sp_with_ial2( :oidc, @@ -773,11 +768,6 @@ def costing_for(cost_type) allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled).and_return( in_person_proofing_opt_in_enabled, ) - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([service_provider.issuer]) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([service_provider.issuer]) allow_any_instance_of(ServiceProvider).to receive(:in_person_proofing_enabled). and_return(false) visit_idp_from_sp_with_ial2( diff --git a/spec/features/idv/doc_auth/how_to_verify_spec.rb b/spec/features/idv/doc_auth/how_to_verify_spec.rb index 48780d589ab..24a5fb5778a 100644 --- a/spec/features/idv/doc_auth/how_to_verify_spec.rb +++ b/spec/features/idv/doc_auth/how_to_verify_spec.rb @@ -20,11 +20,6 @@ } allow_any_instance_of(ServiceProvider).to receive(:in_person_proofing_enabled). and_return(service_provider_in_person_proofing_enabled) - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([ipp_service_provider.issuer]) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([ipp_service_provider.issuer]) visit_idp_from_sp_with_ial2( :oidc, **{ client_id: ipp_service_provider.issuer, facial_match_required: facial_match_required } diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb index f6a26076917..c5694c47a20 100644 --- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb +++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb @@ -343,11 +343,6 @@ def verify_no_upload_photos_section_and_link(page) ) allow_any_instance_of(ServiceProvider).to receive(:in_person_proofing_enabled). and_return(sp_ipp_enabled) - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([service_provider.issuer]) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([service_provider.issuer]) visit_idp_from_sp_with_ial2( :oidc, **{ client_id: service_provider.issuer, diff --git a/spec/features/idv/doc_auth/redo_document_capture_spec.rb b/spec/features/idv/doc_auth/redo_document_capture_spec.rb index 1f960747a40..9460ea01896 100644 --- a/spec/features/idv/doc_auth/redo_document_capture_spec.rb +++ b/spec/features/idv/doc_auth/redo_document_capture_spec.rb @@ -500,11 +500,6 @@ :in_person_proofing_enabled, ).and_return(true) allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(99) - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([ipp_service_provider.issuer]) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([ipp_service_provider.issuer]) perform_in_browser(:mobile) do visit_idp_from_sp_with_ial2( :oidc, @@ -771,11 +766,6 @@ def costing_for(cost_type) allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled).and_return( in_person_proofing_opt_in_enabled, ) - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([service_provider.issuer]) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([service_provider.issuer]) allow_any_instance_of(ServiceProvider).to receive(:in_person_proofing_enabled). and_return(false) visit_idp_from_sp_with_ial2( diff --git a/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb b/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb index 2a66b20974e..db8de8f064d 100644 --- a/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb +++ b/spec/features/idv/steps/in_person_opt_in_ipp_spec.rb @@ -14,9 +14,6 @@ allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled).and_return(true) allow(IdentityConfig.store).to receive(:otp_delivery_blocklist_maxretry).and_return(5) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([ipp_service_provider.issuer, 'urn:gov:gsa:openidconnect:sp:server']) end context 'when ipp_opt_in_enabled and ipp_opt_in_enabled are both enabled' do diff --git a/spec/features/reports/authorization_count_spec.rb b/spec/features/reports/authorization_count_spec.rb index 0686dc40f00..a82067f5d68 100644 --- a/spec/features/reports/authorization_count_spec.rb +++ b/spec/features/reports/authorization_count_spec.rb @@ -186,9 +186,6 @@ def visit_idp_from_ial2_saml_sp(issuer:) before do reset_monthly_auth_count_and_login(user) - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return([client_id_1, client_id_2]) end context 'using oidc' do diff --git a/spec/forms/openid_connect_authorize_form_spec.rb b/spec/forms/openid_connect_authorize_form_spec.rb index 8f3caffcdd0..fe1598234dc 100644 --- a/spec/forms/openid_connect_authorize_form_spec.rb +++ b/spec/forms/openid_connect_authorize_form_spec.rb @@ -198,11 +198,6 @@ end context 'with a known IAL value' do - before do - allow(IdentityConfig.store).to receive( - :allowed_valid_authn_contexts_semantic_providers, - ).and_return(client_id) - end let(:acr_values) do [ 'unknown-value', @@ -252,32 +247,14 @@ ).and_return(false) end - context 'when the service provider is allowed to use facial match ials' do - before do - allow(IdentityConfig.store).to receive( - :allowed_biometric_ial_providers, - ).and_return([client_id]) - end - - it 'succeeds validation' do - expect(form).to be_valid - end - end - - context 'when the service provider is not allowed to use facial match ials' do - it 'fails with a not authorized error' do - expect(form).not_to be_valid - expect(form.errors[:acr_values]). - to include(t('openid_connect.authorization.errors.no_auth')) - end + it 'fails with a not authorized error' do + expect(form).not_to be_valid + expect(form.errors[:acr_values]). + to include(t('openid_connect.authorization.errors.no_auth')) end end context 'when facial match general availability is turned on' do - before do - expect(IdentityConfig.store).not_to receive(:allowed_biometric_ial_providers) - end - it 'succeeds validation' do expect(form).to be_valid end diff --git a/spec/models/service_provider_spec.rb b/spec/models/service_provider_spec.rb index e952fb106c2..387a17e1dbb 100644 --- a/spec/models/service_provider_spec.rb +++ b/spec/models/service_provider_spec.rb @@ -88,24 +88,8 @@ and_return(true) end - context 'when the service provider is in the allowed list' do - before do - expect(IdentityConfig.store).not_to receive(:allowed_biometric_ial_providers) - end - - it 'allows the service provider to use facial match IALs' do - expect(service_provider.facial_match_ial_allowed?).to be(true) - end - end - - context 'when the service provider is not in the allowed list' do - before do - expect(IdentityConfig.store).not_to receive(:allowed_biometric_ial_providers) - end - - it 'allows the service provider to use facial match IALs' do - expect(service_provider.facial_match_ial_allowed?).to be(true) - end + it 'allows the service provider to use facial match IALs' do + expect(service_provider.facial_match_ial_allowed?).to be(true) end end @@ -115,26 +99,8 @@ and_return(false) end - context 'when the service provider is in the allowed list' do - before do - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([service_provider.issuer]) - end - - it 'allows the service provider to use facial match IALs' do - expect(service_provider.facial_match_ial_allowed?).to be(true) - end - end - - context 'when the service provider is not in the allowed list' do - before do - allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). - and_return([]) - end - - it 'does not allow the service provider to use facial match IALs' do - expect(service_provider.facial_match_ial_allowed?).to be(false) - end + it 'does not allow the service provider to use facial match IALs' do + expect(service_provider.facial_match_ial_allowed?).to be(false) end end end diff --git a/spec/services/saml_request_validator_spec.rb b/spec/services/saml_request_validator_spec.rb index 3a6fc370fea..bf530ff771d 100644 --- a/spec/services/saml_request_validator_spec.rb +++ b/spec/services/saml_request_validator_spec.rb @@ -32,9 +32,6 @@ ).and_return( use_vot_in_sp_requests, ) - allow(IdentityConfig.store).to receive( - :allowed_biometric_ial_providers, - ).and_return([issuer]) end context 'valid authn context and sp and authorized nameID format' do From 0a2416006932b92b8ce82c83813b35d46386b132 Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Wed, 20 Nov 2024 14:45:28 -0500 Subject: [PATCH 04/23] LG-14366 Store dynamically-generated proofing components on Profile (#11527) Proofing components are intended to represent the tooling that was used to create a profile. Traditionally these have been generated by tracking proofing components in a database table associated with the user. There were a number of problems with this approach which 5021e2ef8237ecc6ff38f6338371e89fb8dcf06d introduced a `Idv::ProofingComponents` service to address. This service generates proofing components from the session instead of from the database table. This commit uses the `Idv::ProofingComponents` service to generate the proofing components stored on the profile. This will allow us to drop the proofing components table in a future change. [skip changelog] --- .../idv/enter_password_controller.rb | 8 ++- app/services/idv/profile_maker.rb | 9 ++-- app/services/idv/session.rb | 5 +- .../idv/personal_key_controller_spec.rb | 6 ++- spec/policies/idv/flow_policy_spec.rb | 8 ++- spec/services/idv/profile_maker_spec.rb | 12 ++++- spec/services/idv/session_spec.rb | 51 ++++++++++++++----- 7 files changed, 73 insertions(+), 26 deletions(-) diff --git a/app/controllers/idv/enter_password_controller.rb b/app/controllers/idv/enter_password_controller.rb index 101d3fb0cdb..64768ea7ec8 100644 --- a/app/controllers/idv/enter_password_controller.rb +++ b/app/controllers/idv/enter_password_controller.rb @@ -129,7 +129,13 @@ def confirm_current_password def init_profile idv_session.create_profile_from_applicant_with_password( password, - resolved_authn_context_result.enhanced_ipp?, + is_enhanced_ipp: resolved_authn_context_result.enhanced_ipp?, + proofing_components: ProofingComponents.new( + user: current_user, + idv_session:, + session:, + user_session:, + ).to_h, ) if idv_session.verify_by_mail? current_user.send_email_to_all_addresses(:verify_by_mail_letter_requested) diff --git a/app/services/idv/profile_maker.rb b/app/services/idv/profile_maker.rb index 2e9e3a5e2d8..27ef45e8be0 100644 --- a/app/services/idv/profile_maker.rb +++ b/app/services/idv/profile_maker.rb @@ -2,7 +2,7 @@ module Idv class ProfileMaker - attr_reader :pii_attributes + attr_reader :pii_attributes, :proofing_components def initialize( applicant:, @@ -21,13 +21,14 @@ def save_profile( gpo_verification_needed:, in_person_verification_needed:, selfie_check_performed:, + proofing_components:, deactivation_reason: nil ) profile = Profile.new(user: user, active: false, deactivation_reason: deactivation_reason) profile.initiating_service_provider = initiating_service_provider profile.deactivate_for_in_person_verification if in_person_verification_needed profile.encrypt_pii(pii_attributes, user_password) - profile.proofing_components = current_proofing_components + profile.proofing_components = proofing_components profile.fraud_pending_reason = fraud_pending_reason profile.idv_level = set_idv_level( @@ -61,10 +62,6 @@ def set_idv_level(in_person_verification_needed:, selfie_check_performed:) end end - def current_proofing_components - user.proofing_component&.as_json || {} - end - attr_accessor( :user, :user_password, diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb index d31c0d143a5..4f4683997de 100644 --- a/app/services/idv/session.rb +++ b/app/services/idv/session.rb @@ -107,7 +107,9 @@ def respond_to_missing?(method_sym, include_private) VALID_SESSION_ATTRIBUTES.include?(attr_name_sym) || super end - def create_profile_from_applicant_with_password(user_password, is_enhanced_ipp) + def create_profile_from_applicant_with_password( + user_password, is_enhanced_ipp:, proofing_components: + ) if user_has_unscheduled_in_person_enrollment? UspsInPersonProofing::EnrollmentHelper.schedule_in_person_enrollment( user: current_user, @@ -123,6 +125,7 @@ def create_profile_from_applicant_with_password(user_password, is_enhanced_ipp) gpo_verification_needed: !phone_confirmed? || verify_by_mail?, in_person_verification_needed: current_user.has_in_person_enrollment?, selfie_check_performed: session[:selfie_check_performed], + proofing_components:, ) profile.activate unless profile.reason_not_to_activate diff --git a/spec/controllers/idv/personal_key_controller_spec.rb b/spec/controllers/idv/personal_key_controller_spec.rb index dabd49f265e..33946f4bbe1 100644 --- a/spec/controllers/idv/personal_key_controller_spec.rb +++ b/spec/controllers/idv/personal_key_controller_spec.rb @@ -69,7 +69,11 @@ def assert_personal_key_generated_for_profiles(*profile_pii_pairs) idv_session.applicant = applicant if mint_profile_from_idv_session - idv_session.create_profile_from_applicant_with_password(password, is_enhanced_ipp) + idv_session.create_profile_from_applicant_with_password( + password, + is_enhanced_ipp:, + proofing_components: {}, + ) end end diff --git a/spec/policies/idv/flow_policy_spec.rb b/spec/policies/idv/flow_policy_spec.rb index d96c9006355..8561f382d84 100644 --- a/spec/policies/idv/flow_policy_spec.rb +++ b/spec/policies/idv/flow_policy_spec.rb @@ -315,7 +315,9 @@ it 'returns personal_key' do stub_up_to(:request_letter, idv_session: idv_session) idv_session.gpo_code_verified = true - idv_session.create_profile_from_applicant_with_password('password', is_enhanced_ipp) + idv_session.create_profile_from_applicant_with_password( + 'password', is_enhanced_ipp:, proofing_components: {} + ) expect(subject.info_for_latest_step.key).to eq(:personal_key) expect(subject.controller_allowed?(controller: Idv::PersonalKeyController)).to be @@ -326,7 +328,9 @@ let(:is_enhanced_ipp) { false } it 'returns personal_key' do stub_up_to(:otp_verification, idv_session: idv_session) - idv_session.create_profile_from_applicant_with_password('password', is_enhanced_ipp) + idv_session.create_profile_from_applicant_with_password( + 'password', is_enhanced_ipp:, proofing_components: {} + ) expect(subject.info_for_latest_step.key).to eq(:personal_key) expect(subject.controller_allowed?(controller: Idv::PersonalKeyController)).to be diff --git a/spec/services/idv/profile_maker_spec.rb b/spec/services/idv/profile_maker_spec.rb index 6736bcba24e..987438eff83 100644 --- a/spec/services/idv/profile_maker_spec.rb +++ b/spec/services/idv/profile_maker_spec.rb @@ -7,6 +7,7 @@ let(:user_password) { user.password } let(:initiating_service_provider) { nil } let(:in_person_proofing_enforce_tmx_mock) { false } + let(:proofing_components) { { document_check: :mock } } subject do described_class.new( @@ -18,12 +19,12 @@ end it 'creates an inactive Profile with encrypted PII' do - proofing_component = ProofingComponent.create(user_id: user.id, document_check: 'mock') profile = subject.save_profile( fraud_pending_reason: nil, gpo_verification_needed: false, in_person_verification_needed: false, selfie_check_performed: false, + proofing_components:, ) pii = subject.pii_attributes @@ -32,7 +33,7 @@ expect(profile.encrypted_pii).to_not be_nil expect(profile.encrypted_pii).to_not match('Some') expect(profile.fraud_pending_reason).to be_nil - expect(profile.proofing_components).to match(proofing_component.as_json) + expect(profile.proofing_components).to match(proofing_components.as_json) expect(profile.active).to eq(false) expect(profile.deactivation_reason).to be_nil @@ -48,6 +49,7 @@ deactivation_reason: :encryption_error, in_person_verification_needed: false, selfie_check_performed: false, + proofing_components:, ) end it 'creates an inactive profile with deactivation reason' do @@ -75,6 +77,7 @@ deactivation_reason: nil, in_person_verification_needed: in_person_verification_needed, selfie_check_performed: false, + proofing_components:, ) end @@ -118,6 +121,7 @@ deactivation_reason: nil, in_person_verification_needed: false, selfie_check_performed: false, + proofing_components:, ) end it 'creates a pending profile for gpo verification' do @@ -151,6 +155,7 @@ deactivation_reason: nil, in_person_verification_needed: true, selfie_check_performed: false, + proofing_components:, ) end @@ -190,6 +195,7 @@ deactivation_reason: nil, in_person_verification_needed: true, selfie_check_performed: false, + proofing_components:, ) end @@ -220,6 +226,7 @@ deactivation_reason: nil, in_person_verification_needed: false, selfie_check_performed: selfie_check_performed, + proofing_components:, ) end @@ -267,6 +274,7 @@ deactivation_reason: nil, in_person_verification_needed: false, selfie_check_performed: false, + proofing_components:, ) end it 'creates a profile with the initiating sp recorded' do diff --git a/spec/services/idv/session_spec.rb b/spec/services/idv/session_spec.rb index e7009ef5352..df023971d8d 100644 --- a/spec/services/idv/session_spec.rb +++ b/spec/services/idv/session_spec.rb @@ -3,7 +3,6 @@ RSpec.describe Idv::Session do let(:user) { create(:user) } let(:user_session) { {} } - let(:is_enhanced_ipp) { false } subject do Idv::Session.new(user_session: user_session, current_user: user, service_provider: nil) @@ -128,6 +127,8 @@ end describe '#create_profile_from_applicant_with_password' do + let(:is_enhanced_ipp) { false } + let(:proofing_components) { { document_check: 'mock' } } let(:opt_in_param) { nil } before do @@ -146,7 +147,9 @@ now = Time.zone.now subject.user_phone_confirmation = true - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) profile = subject.profile expect(profile.activated_at).to eq now @@ -165,7 +168,9 @@ it 'does not complete the profile if the user has not completed OTP phone confirmation' do subject.user_phone_confirmation = nil - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) profile = subject.profile expect(profile.activated_at).to eq nil @@ -200,7 +205,9 @@ end it 'creates an USPS enrollment' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) expect(UspsInPersonProofing::EnrollmentHelper).to have_received( :schedule_in_person_enrollment, ).with( @@ -212,7 +219,9 @@ end it 'creates a profile with in person verification pending' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) expect(profile).to have_attributes( { activated_at: nil, @@ -227,12 +236,16 @@ end it 'saves the pii to the session' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) expect(Pii::Cacher.new(user, user_session).fetch(profile.id)).to_not be_nil end it 'associates the in person enrollment with the created profile' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) expect(enrollment.reload.profile_id).to eq(profile.id) end end @@ -245,7 +258,9 @@ end it 'does not create a profile' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) rescue expect(profile).to be_nil end @@ -264,14 +279,18 @@ end it 'does not create an USPS enrollment' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) expect(UspsInPersonProofing::EnrollmentHelper).to_not have_received( :schedule_in_person_enrollment, ) end it 'creates a profile' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) expect(profile).to have_attributes( { active: true, @@ -284,7 +303,9 @@ end it 'saves the pii to the session' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) expect(Pii::Cacher.new(user, user_session).fetch(profile.id)).to_not be_nil end end @@ -297,7 +318,9 @@ end it 'sets profile to pending gpo verification' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) profile = subject.profile expect(profile.activated_at).to eq nil @@ -321,7 +344,9 @@ end it 'does not complete the user profile' do - subject.create_profile_from_applicant_with_password(user.password, is_enhanced_ipp) + subject.create_profile_from_applicant_with_password( + user.password, is_enhanced_ipp:, proofing_components: + ) profile = subject.profile expect(profile.activated_at).to eq nil From 362dc902095cc9d6c97122edfe69604d253113e0 Mon Sep 17 00:00:00 2001 From: Doug Price Date: Wed, 20 Nov 2024 18:17:22 -0500 Subject: [PATCH 05/23] LG-15125: Wait for Socure result - hybrid flow (#11530) * LG-15125: Wait for Socure result - hybrid flow As in #11500 for the standard flow, we need to wait for the Socure result to be received before we can determine where to send the user. Currently, if the wait times out, we show a placeholder plain text of "Technical difficulties!!!" to be replaced with the appropriate "Try again?" page in a following ticket. Note there is some identical code in the standard vs hybrid controllers that might be refactored into a concern in the future, but until all of the paths are complete, I thought it best to live with the duplication for now. [skip changelog] --- .../socure/document_capture_controller.rb | 50 ++++++++++++++++++- .../idv/socure/document_capture_controller.rb | 38 +++++++------- app/services/analytics_events.rb | 4 +- .../document_capture_controller_spec.rb | 27 ++++++++++ 4 files changed, 98 insertions(+), 21 deletions(-) diff --git a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb index 6dfd7980241..243c4e5d598 100644 --- a/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/socure/document_capture_controller.rb @@ -4,11 +4,10 @@ module Idv module HybridMobile module Socure class DocumentCaptureController < ApplicationController - include Idv::AvailabilityConcern + include AvailabilityConcern include DocumentCaptureConcern include Idv::HybridMobile::HybridMobileConcern include RenderConditionConcern - include DocumentCaptureConcern check_or_render_not_found -> { IdentityConfig.store.socure_docv_enabled } before_action :check_valid_document_capture_session, except: [:update] @@ -50,10 +49,16 @@ def show end def update + return if wait_for_result? + result = handle_stored_result( user: document_capture_session.user, store_in_session: false, ) + # TODO: new analytics event? + analytics.idv_doc_auth_document_capture_submitted( + **result.to_h.merge(analytics_arguments), + ) if result.success? redirect_to idv_hybrid_mobile_capture_complete_url @@ -61,6 +66,47 @@ def update redirect_to idv_hybrid_mobile_socure_document_capture_url end end + + private + + def wait_for_result? + return false if stored_result.present? + + # If the stored_result is nil, the job fetching the results has not completed. + analytics.idv_doc_auth_document_capture_polling_wait_visited(**analytics_arguments) + if wait_timed_out? + # flash[:error] = I18n.t('errors.doc_auth.polling_timeout') + # TODO: redirect to try again page LG-14873/14952/15059 + render plain: 'Technical difficulties!!!', status: :ok + else + @refresh_interval = + IdentityConfig.store.doc_auth_socure_wait_polling_refresh_max_seconds + render 'idv/socure/document_capture/wait' + end + + true + end + + def wait_timed_out? + if session[:socure_docv_wait_polling_started_at].nil? + session[:socure_docv_wait_polling_started_at] = Time.zone.now.to_s + return false + end + start = DateTime.parse(session[:socure_docv_wait_polling_started_at]) + timeout_period = + IdentityConfig.store.doc_auth_socure_wait_polling_timeout_minutes.minutes || 2.minutes + start + timeout_period < Time.zone.now + end + + def analytics_arguments + { + flow_path: 'hybrid', + step: 'socure_document_capture', + analytics_id: 'Doc Auth', + liveness_checking_required: false, + selfie_check_required: false, + } + end end end end diff --git a/app/controllers/idv/socure/document_capture_controller.rb b/app/controllers/idv/socure/document_capture_controller.rb index b93ec65334c..1f764f64856 100644 --- a/app/controllers/idv/socure/document_capture_controller.rb +++ b/app/controllers/idv/socure/document_capture_controller.rb @@ -62,27 +62,13 @@ def show end def update + return if wait_for_result? + clear_future_steps! idv_session.redo_document_capture = nil # done with this redo # Not used in standard flow, here for data consistency with hybrid flow. document_capture_session.confirm_ocr - # If the stored_result is nil, the job fetching the results has not completed. - if stored_result.nil? - analytics.idv_doc_auth_document_capture_polling_wait_visited(**analytics_arguments) - if wait_timed_out? - # flash[:error] = I18n.t('errors.doc_auth.polling_timeout') - # TODO: redirect to try again page LG-14873/14952/15059 - render plain: 'Technical difficulties!!!', status: :ok - else - @refresh_interval = - IdentityConfig.store.doc_auth_socure_wait_polling_refresh_max_seconds - render 'idv/socure/document_capture/wait' - end - - return - end - result = handle_stored_result # TODO: new analytics event? analytics.idv_doc_auth_document_capture_submitted(**result.to_h.merge(analytics_arguments)) @@ -121,6 +107,24 @@ def self.step_info private + def wait_for_result? + return false if stored_result.present? + + # If the stored_result is nil, the job fetching the results has not completed. + analytics.idv_doc_auth_document_capture_polling_wait_visited(**analytics_arguments) + if wait_timed_out? + # flash[:error] = I18n.t('errors.doc_auth.polling_timeout') + # TODO: redirect to try again page LG-14873/14952/15059 + render plain: 'Technical difficulties!!!', status: :ok + else + @refresh_interval = + IdentityConfig.store.doc_auth_socure_wait_polling_refresh_max_seconds + render 'idv/socure/document_capture/wait' + end + + true + end + def wait_timed_out? if idv_session.socure_docv_wait_polling_started_at.nil? idv_session.socure_docv_wait_polling_started_at = Time.zone.now.to_s @@ -128,7 +132,7 @@ def wait_timed_out? end start = DateTime.parse(idv_session.socure_docv_wait_polling_started_at) timeout_period = - IdentityConfig.store.doc_auth_socure_wait_polling_timeout_minutes.minutes || 5.minutes + IdentityConfig.store.doc_auth_socure_wait_polling_timeout_minutes.minutes || 2.minutes start + timeout_period < Time.zone.now end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index f5ace1ca583..2cbeac6bd13 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1255,10 +1255,10 @@ def idv_doc_auth_document_capture_polling_wait_visited( flow_path:, step:, analytics_id:, - redo_document_capture:, - skip_hybrid_handoff:, liveness_checking_required:, selfie_check_required:, + redo_document_capture: nil, + skip_hybrid_handoff: nil, opted_in_to_in_person_proofing: nil, acuant_sdk_upgrade_ab_test_bucket: nil, **extra diff --git a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb index 60b77e84bdb..ba96dbcfbb4 100644 --- a/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb +++ b/spec/controllers/idv/hybrid_mobile/socure/document_capture_controller_spec.rb @@ -29,6 +29,8 @@ session[:doc_capture_user_id] = user&.id session[:document_capture_session_uuid] = document_capture_session_uuid + + stub_analytics end describe 'before_actions' do @@ -273,6 +275,7 @@ get(:update) expect(response).to redirect_to(idv_hybrid_mobile_capture_complete_url) + expect(@analytics).to have_logged_event('IdV: doc auth document_capture submitted') end context 'when socure is disabled' do @@ -298,6 +301,30 @@ get(:update) expect(response).to redirect_to(idv_hybrid_mobile_socure_document_capture_url) + expect(@analytics).to have_logged_event('IdV: doc auth document_capture submitted') + end + end + + context 'when stored result is nil' do + let(:stored_result) { nil } + + it 'renders the wait view' do + get(:update) + + expect(response).to render_template('idv/socure/document_capture/wait') + expect(@analytics).to have_logged_event(:idv_doc_auth_document_capture_polling_wait_visited) + end + + context 'when the wait times out' do + before do + allow(subject).to receive(:wait_timed_out?).and_return(true) + end + + it 'renders a technical difficulties message' do + get(:update) + expect(response).to have_http_status(:ok) + expect(response.body).to eq('Technical difficulties!!!') + end end end end From 56ba291485563c591f001f27ba0b78fb7a87b4a0 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Thu, 21 Nov 2024 08:17:32 -0500 Subject: [PATCH 06/23] Remove allowed_extra_analytics (#11536) changelog: Internal, Analytics, Document analytics event parameters --- app/services/analytics_events.rb | 66 +++++++++++++++---- .../idv/doc_auth/verify_info_step_spec.rb | 2 +- spec/features/idv/end_to_end_idv_spec.rb | 2 +- spec/features/idv/in_person_spec.rb | 2 +- .../idv/steps/enter_code_step_spec.rb | 2 +- .../idv/steps/resend_letter_step_spec.rb | 2 +- spec/features/saml/ial2_sso_spec.rb | 2 +- spec/forms/idv/api_image_upload_form_spec.rb | 2 +- spec/services/idv/phone_step_spec.rb | 2 +- 9 files changed, 61 insertions(+), 21 deletions(-) diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 2cbeac6bd13..e11b752cf28 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1379,10 +1379,35 @@ def idv_doc_auth_exception_visited(step_name:, remaining_submit_attempts:, **ext end # @param [String] side the side of the image submission - def idv_doc_auth_failed_image_resubmitted(side:, **extra) + # @param [Integer] submit_attempts Times that user has tried submitting (previously called + # "attempts") + # @param [Integer] remaining_submit_attempts (previously called "remaining_attempts") + # @param ["hybrid","standard"] flow_path Document capture user flow + # @param [String] liveness_checking_required Whether or not the selfie is required + # @param [String] front_image_fingerprint Fingerprint of front image data + # @param [String] back_image_fingerprint Fingerprint of back image data + # @param [String] selfie_image_fingerprint Fingerprint of selfie image data + def idv_doc_auth_failed_image_resubmitted( + side:, + remaining_submit_attempts:, + flow_path:, + liveness_checking_required:, + submit_attempts:, + front_image_fingerprint:, + back_image_fingerprint:, + selfie_image_fingerprint:, + **extra + ) track_event( 'IdV: failed doc image resubmitted', - side: side, + side:, + remaining_submit_attempts:, + flow_path:, + liveness_checking_required:, + submit_attempts:, + front_image_fingerprint:, + back_image_fingerprint:, + selfie_image_fingerprint:, **extra, ) end @@ -3430,15 +3455,18 @@ def idv_in_person_usps_proofing_results_job_please_call_email_initiated( # GetUspsProofingResultsJob is beginning. Includes some metadata about what the job will do # @param [Integer] enrollments_count number of enrollments eligible for status check # @param [Integer] reprocess_delay_minutes minimum delay since last status check + # @param [String] job_name Name of class which triggered proofing job def idv_in_person_usps_proofing_results_job_started( enrollments_count:, reprocess_delay_minutes:, + job_name:, **extra ) track_event( 'GetUspsProofingResultsJob: Job started', - enrollments_count: enrollments_count, - reprocess_delay_minutes: reprocess_delay_minutes, + enrollments_count:, + reprocess_delay_minutes:, + job_name:, **extra, ) end @@ -4330,12 +4358,15 @@ def idv_request_letter_visited( end # GPO "resend letter" page visited + # @param [String,nil] pending_profile_idv_level ID verification level of user's pending profile. # @identity.idp.previous_event_name IdV: request letter visited def idv_resend_letter_visited( + pending_profile_idv_level: nil, **extra ) track_event( :idv_resend_letter_visited, + pending_profile_idv_level:, **extra, ) end @@ -6158,6 +6189,7 @@ def proofing_address_result_missing # @param [String] phone_fingerprint HMAC fingerprint of the phone number formatted as E.164 # @param ["authentication", "reauthentication", "confirmation"] context User session context # @param ["sms", "voice"] otp_delivery_preference Channel used to send the message + # @param [String,nil] step_name Name of step in user flow where rate limit occurred # @identity.idp.previous_event_name Throttler Rate Limit Triggered def rate_limit_reached( limiter_type:, @@ -6165,6 +6197,7 @@ def rate_limit_reached( phone_fingerprint: nil, context: nil, otp_delivery_preference: nil, + step_name: nil, **extra ) track_event( @@ -6174,6 +6207,7 @@ def rate_limit_reached( phone_fingerprint:, context:, otp_delivery_preference:, + step_name:, **extra, ) end @@ -6863,16 +6897,20 @@ def user_registration_2fa_setup_visit( # reason for the consent screen being shown # @param [Boolean] in_account_creation_flow Whether user is going through account creation # @param [Array] sp_session_requested_attributes Attributes requested by the service provider + # @param [String, nil] in_person_proofing_status In person proofing status + # @param [String, nil] doc_auth_result The doc auth result def user_registration_agency_handoff_page_visit( - ial2:, - service_provider_name:, - page_occurence:, - needs_completion_screen_reason:, - in_account_creation_flow:, - sp_session_requested_attributes:, - ialmax: nil, - **extra - ) + ial2:, + service_provider_name:, + page_occurence:, + needs_completion_screen_reason:, + in_account_creation_flow:, + sp_session_requested_attributes:, + ialmax: nil, + in_person_proofing_status: nil, + doc_auth_result: nil, + **extra + ) track_event( 'User registration: agency handoff visited', ial2:, @@ -6882,6 +6920,8 @@ def user_registration_agency_handoff_page_visit( needs_completion_screen_reason:, in_account_creation_flow:, sp_session_requested_attributes:, + in_person_proofing_status:, + doc_auth_result:, **extra, ) end diff --git a/spec/features/idv/doc_auth/verify_info_step_spec.rb b/spec/features/idv/doc_auth/verify_info_step_spec.rb index f002b2c5005..661c75dc0f9 100644 --- a/spec/features/idv/doc_auth/verify_info_step_spec.rb +++ b/spec/features/idv/doc_auth/verify_info_step_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'verify_info step and verify_info_concern', :js, allowed_extra_analytics: [:*] do +RSpec.feature 'verify_info step and verify_info_concern', :js do include IdvStepHelper include DocAuthHelper diff --git a/spec/features/idv/end_to_end_idv_spec.rb b/spec/features/idv/end_to_end_idv_spec.rb index 055e011cb7a..dc2c5123915 100644 --- a/spec/features/idv/end_to_end_idv_spec.rb +++ b/spec/features/idv/end_to_end_idv_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe 'Identity verification', :js, allowed_extra_analytics: [:*] do +RSpec.describe 'Identity verification', :js do include IdvStepHelper include InPersonHelper diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb index df4f09ae64a..68bd0646446 100644 --- a/spec/features/idv/in_person_spec.rb +++ b/spec/features/idv/in_person_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' require 'axe-rspec' -RSpec.describe 'In Person Proofing', js: true, allowed_extra_analytics: [:*] do +RSpec.describe 'In Person Proofing', js: true do include IdvStepHelper include SpAuthHelper include InPersonHelper diff --git a/spec/features/idv/steps/enter_code_step_spec.rb b/spec/features/idv/steps/enter_code_step_spec.rb index f12874565c0..a7b0a6a423a 100644 --- a/spec/features/idv/steps/enter_code_step_spec.rb +++ b/spec/features/idv/steps/enter_code_step_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'idv enter letter code step', allowed_extra_analytics: [:*] do +RSpec.feature 'idv enter letter code step' do include IdvStepHelper let(:otp) { 'ABC123' } diff --git a/spec/features/idv/steps/resend_letter_step_spec.rb b/spec/features/idv/steps/resend_letter_step_spec.rb index ab10aae5d6c..1d4a4fe1337 100644 --- a/spec/features/idv/steps/resend_letter_step_spec.rb +++ b/spec/features/idv/steps/resend_letter_step_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'idv resend letter step', allowed_extra_analytics: [:*] do +RSpec.feature 'idv resend letter step' do include IdvStepHelper include OidcAuthHelper diff --git a/spec/features/saml/ial2_sso_spec.rb b/spec/features/saml/ial2_sso_spec.rb index 4d54ee8798a..5c772f42fa7 100644 --- a/spec/features/saml/ial2_sso_spec.rb +++ b/spec/features/saml/ial2_sso_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'IAL2 Single Sign On', allowed_extra_analytics: [:*] do +RSpec.feature 'IAL2 Single Sign On' do include SamlAuthHelper include IdvStepHelper include DocAuthHelper diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index 03ac3d58c2d..422527d7b89 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe Idv::ApiImageUploadForm, allowed_extra_analytics: [:*] do +RSpec.describe Idv::ApiImageUploadForm do include DocPiiHelper subject(:form) do diff --git a/spec/services/idv/phone_step_spec.rb b/spec/services/idv/phone_step_spec.rb index a5213b09c6f..a98978b034c 100644 --- a/spec/services/idv/phone_step_spec.rb +++ b/spec/services/idv/phone_step_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe Idv::PhoneStep, allowed_extra_analytics: [:*] do +RSpec.describe Idv::PhoneStep do let(:user) { create(:user) } let(:service_provider) do create( From 656c479086d2bd5b5e26cdae74f51093943a7121 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:50:19 -0500 Subject: [PATCH 07/23] Remove unused allowed_extra_analytics (#11537) changelog: Internal, Analytics, Document analytics event parameters --- spec/features/openid_connect/openid_connect_spec.rb | 4 +--- spec/features/saml/vtr_spec.rb | 10 +++------- spec/features/sign_in/multiple_vot_spec.rb | 8 ++------ spec/features/users/user_profile_spec.rb | 4 +--- 4 files changed, 7 insertions(+), 19 deletions(-) diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb index ede6d47d923..35ee5556c11 100644 --- a/spec/features/openid_connect/openid_connect_spec.rb +++ b/spec/features/openid_connect/openid_connect_spec.rb @@ -601,9 +601,7 @@ to include('Verified within value must be at least 30 days or older') end - it 'sends the user through idv again via verified_within param', - :js, - allowed_extra_analytics: [:*] do + it 'sends the user through idv again via verified_within param', :js do client_id = 'urn:gov:gsa:openidconnect:sp:server' allow(IdentityConfig.store).to receive(:allowed_verified_within_providers). and_return([client_id]) diff --git a/spec/features/saml/vtr_spec.rb b/spec/features/saml/vtr_spec.rb index 7321a806b07..a22bf2781ac 100644 --- a/spec/features/saml/vtr_spec.rb +++ b/spec/features/saml/vtr_spec.rb @@ -126,9 +126,7 @@ expect_successful_saml_redirect end - scenario 'sign in with VTR request for idv requires idv', - :js, - allowed_extra_analytics: [:*] do + scenario 'sign in with VTR request for idv requires idv', :js do user = create(:user, :fully_registered) visit_saml_authn_request_url( @@ -150,8 +148,7 @@ expect_successful_saml_redirect end - scenario 'sign in with VTR request for idv includes proofed attributes', - allowed_extra_analytics: [:*] do + scenario 'sign in with VTR request for idv includes proofed attributes' do pii = { first_name: 'Jonathan', ssn: '900-66-6666', @@ -190,8 +187,7 @@ end scenario 'sign in with VTR request for idv with facial match requires idv with facial match', - :js, - allowed_extra_analytics: [:*] do + :js do user = create(:user, :proofed) user.active_profile.update!(idv_level: :legacy_unsupervised) diff --git a/spec/features/sign_in/multiple_vot_spec.rb b/spec/features/sign_in/multiple_vot_spec.rb index 8145eee78cb..9210fae3d17 100644 --- a/spec/features/sign_in/multiple_vot_spec.rb +++ b/spec/features/sign_in/multiple_vot_spec.rb @@ -38,9 +38,7 @@ expect(user_info[:vot]).to eq('C1.C2.P1') end - scenario 'identity proofing with facial match is required if user is not proofed', - :js, - allowed_extra_analytics: [:*] do + scenario 'identity proofing with facial match is required if user is not proofed', :js do user = create(:user, :fully_registered) visit_idp_from_oidc_sp_with_vtr(vtr: ['C1.C2.P1.Pb', 'C1.C2.P1']) @@ -176,9 +174,7 @@ expect(first_name).to_not be_blank end - scenario 'identity proofing with facial match is required if user is not proofed', - :js, - allowed_extra_analytics: [:*] do + scenario 'identity proofing with facial match is required if user is not proofed', :js do user = create(:user, :fully_registered) visit_saml_authn_request_url( diff --git a/spec/features/users/user_profile_spec.rb b/spec/features/users/user_profile_spec.rb index 76c594f8e1f..32399dede83 100644 --- a/spec/features/users/user_profile_spec.rb +++ b/spec/features/users/user_profile_spec.rb @@ -158,9 +158,7 @@ expect(current_path).to eq(account_path) end - it 'allows the user reactivate their profile by reverifying', - :js, - allowed_extra_analytics: [:*] do + it 'allows the user reactivate their profile by reverifying', :js do profile = create(:profile, :active, :verified, pii: { ssn: '1234', dob: '1920-01-01' }) user = profile.user From 6dc9aa5e2b9e6be21b0b8af53b106e2a718690ce Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:06:37 -0500 Subject: [PATCH 08/23] Revert logging whether password matches current for password reset event (#11457) * Revert "Log if password matches current for password reset event (#11423)" This reverts commit a419d8c6959db8727c1ff0c7ac29b4c347d6fc8c. * Remove inaccurate route verb Co-authored-by: Zach Margolis * Refactor form specs for updated expectations * Remove spec expectations for password_matches_existing --------- Co-authored-by: Zach Margolis --- .../users/reset_passwords_controller.rb | 10 +--- app/forms/reset_password_form.rb | 8 +-- app/services/analytics_events.rb | 3 -- config/application.yml.default | 1 - config/initializers/ab_tests.rb | 6 --- lib/identity_config.rb | 1 - .../users/reset_passwords_controller_spec.rb | 54 ------------------- spec/forms/reset_password_form_spec.rb | 24 +-------- 8 files changed, 3 insertions(+), 104 deletions(-) diff --git a/app/controllers/users/reset_passwords_controller.rb b/app/controllers/users/reset_passwords_controller.rb index 49d1b6374f8..e609bd5b0df 100644 --- a/app/controllers/users/reset_passwords_controller.rb +++ b/app/controllers/users/reset_passwords_controller.rb @@ -3,8 +3,6 @@ module Users class ResetPasswordsController < Devise::PasswordsController include AuthorizationCountConcern - include AbTestingConcern - before_action :store_sp_metadata_in_session, only: [:edit] before_action :store_token_in_session, only: [:edit] @@ -44,13 +42,7 @@ def edit def update self.resource = user_matching_token(user_params[:reset_password_token]) - @reset_password_form = ResetPasswordForm.new( - user: resource, - log_password_matches_existing: ab_test_bucket( - :LOG_PASSWORD_RESET_MATCHES_EXISTING, - user: resource, - ) == :log, - ) + @reset_password_form = ResetPasswordForm.new(user: resource) result = @reset_password_form.submit(user_params) diff --git a/app/forms/reset_password_form.rb b/app/forms/reset_password_form.rb index 35d8ff890d1..adcf084577e 100644 --- a/app/forms/reset_password_form.rb +++ b/app/forms/reset_password_form.rb @@ -5,14 +5,11 @@ class ResetPasswordForm include FormPasswordValidator attr_accessor :reset_password_token - private attr_reader :log_password_matches_existing - alias_method :log_password_matches_existing?, :log_password_matches_existing validate :valid_token - def initialize(user:, log_password_matches_existing: false) + def initialize(user:) @user = user - @log_password_matches_existing = log_password_matches_existing @reset_password_token = @user.reset_password_token @validate_confirmation = true @active_profile = user.active_profile @@ -50,8 +47,6 @@ def handle_valid_password end def update_user - @password_matches_existing = user.valid_password?(password) if log_password_matches_existing? - attributes = { password: password } ActiveRecord::Base.transaction do @@ -92,7 +87,6 @@ def extra_analytics_attributes { user_id: user.uuid, profile_deactivated: active_profile.present?, - password_matches_existing: @password_matches_existing, pending_profile_invalidated: pending_profile.present?, pending_profile_pending_reasons: (pending_profile&.pending_reasons || [])&.join(','), } diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index e11b752cf28..9efc2ca3f4d 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -5828,7 +5828,6 @@ def password_reset_email( # @param [String] pending_profile_pending_reasons Comma-separated list of the pending states # associated with the associated profile. # @param [Hash] error_details Details for errors that occurred in unsuccessful submission - # @param [Boolean] password_matches_existing Whether the password is same as the user's current # The user changed the password for their account via the password reset flow def password_reset_password( success:, @@ -5836,7 +5835,6 @@ def password_reset_password( profile_deactivated:, pending_profile_invalidated:, pending_profile_pending_reasons:, - password_matches_existing:, error_details: {}, **extra ) @@ -5848,7 +5846,6 @@ def password_reset_password( profile_deactivated:, pending_profile_invalidated:, pending_profile_pending_reasons:, - password_matches_existing:, **extra, ) end diff --git a/config/application.yml.default b/config/application.yml.default index 2cb0f53eaf1..eb65bbc102e 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -228,7 +228,6 @@ lexisnexis_trueid_username: test_username lexisnexis_username: test_username ################################################################### lockout_period_in_minutes: 10 -log_password_reset_matches_existing_ab_test_percent: 0 log_to_stdout: false login_otp_confirmation_max_attempts: 10 logins_per_email_and_ip_bantime: 60 diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index 952a7071214..cb2931bac37 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -82,12 +82,6 @@ def self.all end end.freeze - LOG_PASSWORD_RESET_MATCHES_EXISTING = AbTest.new( - experiment_name: 'Log password_matches_existing event property on password reset', - should_log: ['Password Reset: Password Submitted'].to_set, - buckets: { log: IdentityConfig.store.log_password_reset_matches_existing_ab_test_percent }, - ).freeze - RECOMMEND_WEBAUTHN_PLATFORM_FOR_SMS_USER = AbTest.new( experiment_name: 'Recommend Face or Touch Unlock for SMS users', should_log: [ diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 35957cc6a58..44938a1fc13 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -242,7 +242,6 @@ def self.store config.add(:lexisnexis_trueid_username, type: :string) config.add(:lexisnexis_username, type: :string) config.add(:lockout_period_in_minutes, type: :integer) - config.add(:log_password_reset_matches_existing_ab_test_percent, type: :integer) config.add(:log_to_stdout, type: :boolean) config.add(:login_otp_confirmation_max_attempts, type: :integer) config.add(:logins_per_email_and_ip_bantime, type: :integer) diff --git a/spec/controllers/users/reset_passwords_controller_spec.rb b/spec/controllers/users/reset_passwords_controller_spec.rb index d3563f9ff27..9f08e5edf0b 100644 --- a/spec/controllers/users/reset_passwords_controller_spec.rb +++ b/spec/controllers/users/reset_passwords_controller_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' RSpec.describe Users::ResetPasswordsController, devise: true do - include AbTestsHelper - let(:password_error_message) do t('errors.attributes.password.too_short.other', count: Devise.password_length.first) end @@ -404,58 +402,6 @@ expect(user.active_profile.present?).to eq false expect(response).to redirect_to new_user_session_path end - - context 'proofed user submits same password as current' do - let(:user) { create(:user, :proofed) } - let(:password) { user.password } - - it 'logs event indicating profile deactivated while password the same' do - stub_analytics - - reset_password_token = user.set_reset_password_token - - put :update, params: { - reset_password_form: { - password:, - password_confirmation: password, - reset_password_token:, - }, - } - - expect(@analytics).to have_logged_event( - 'Password Reset: Password Submitted', - hash_not_including(password_matches_existing: be_in([true, false])), - ) - end - - context 'when in ab test for logging password matches existing' do - before do - allow(controller).to receive(:ab_test_bucket).with( - :LOG_PASSWORD_RESET_MATCHES_EXISTING, - user:, - ).and_return(:log) - end - - it 'logs event indicating profile deactivated while password the same' do - stub_analytics - - reset_password_token = user.set_reset_password_token - - put :update, params: { - reset_password_form: { - password:, - password_confirmation: password, - reset_password_token:, - }, - } - - expect(@analytics).to have_logged_event( - 'Password Reset: Password Submitted', - hash_including(profile_deactivated: true, password_matches_existing: true), - ) - end - end - end end context 'unconfirmed user submits valid new password' do diff --git a/spec/forms/reset_password_form_spec.rb b/spec/forms/reset_password_form_spec.rb index df25444e89f..a9c588cd2e6 100644 --- a/spec/forms/reset_password_form_spec.rb +++ b/spec/forms/reset_password_form_spec.rb @@ -2,8 +2,7 @@ RSpec.describe ResetPasswordForm, type: :model do let(:user) { create(:user, uuid: '123') } - let(:log_password_matches_existing) { false } - subject(:form) { ResetPasswordForm.new(user:, log_password_matches_existing:) } + subject(:form) { ResetPasswordForm.new(user:) } let(:password) { 'a good and powerful password' } let(:password_confirmation) { password } @@ -33,7 +32,6 @@ profile_deactivated: false, pending_profile_invalidated: false, pending_profile_pending_reasons: '', - password_matches_existing: nil, ) end end @@ -64,7 +62,6 @@ profile_deactivated: false, pending_profile_invalidated: false, pending_profile_pending_reasons: '', - password_matches_existing: nil, ) end end @@ -84,7 +81,6 @@ profile_deactivated: false, pending_profile_invalidated: false, pending_profile_pending_reasons: '', - password_matches_existing: nil, ) end end @@ -117,7 +113,6 @@ profile_deactivated: false, pending_profile_invalidated: false, pending_profile_pending_reasons: '', - password_matches_existing: nil, ) end end @@ -134,7 +129,6 @@ profile_deactivated: false, pending_profile_invalidated: false, pending_profile_pending_reasons: '', - password_matches_existing: nil, ) end end @@ -147,22 +141,6 @@ expect(result.extra[:profile_deactivated]).to eq(true) expect(user.profiles.any?(&:active?)).to eq(false) end - - context 'when the password is same as current' do - let(:password) { user.password } - - it 'does not include extra detail for password matching existing' do - expect(result.extra[:password_matches_existing]).to eq(nil) - end - - context 'when initialized to log password_matches_existing' do - let(:log_password_matches_existing) { true } - - it 'includes extra detail for password matching existing' do - expect(result.extra[:password_matches_existing]).to eq(true) - end - end - end end context 'when the user does not have an active profile' do From bc88339bb144cfb077e959f8399c23e8dd831013 Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey Date: Thu, 21 Nov 2024 14:15:18 -0500 Subject: [PATCH 09/23] LG-15016: Socure document capture feature specs starter (#11529) * helper to send webhooks during feature testing * init socure feature test * remove unused functions and working happy path * rebase - merege conflict resolved * rebase - merege conflict resolved * make stub_docv_document_request dynamic * spec for re-routing lost socure hybrid flow user * move socure doc auth test helprs to to doc cap step helper * add feature spec for socure hybrid mobile * socure hybrid mobile w/o allow_net_connect_on_start * fix specs * remove unused code and allow mock requests * [skip changelog] * remove commented lines * send all docv webhook endpoints when uploading documents * happy linting * happy linting take 2 * remove uneeded before block statement * use new renamed socure env vars * use new renamed socure env vars socure_docv_document_request_endpoint * alphabetized socure_docv_document_request_endpoint in yaml config --- config/application.yml.default | 2 + .../doc_auth/socure_document_capture_spec.rb | 209 ++++++++++++++ spec/features/idv/hybrid_mobile/entry_spec.rb | 23 ++ .../hybrid_socure_mobile_spec.rb | 255 ++++++++++++++++++ spec/fixtures/socure_docv/pass.json | 41 +++ .../features/document_capture_step_helper.rb | 69 +++++ spec/support/socure_docv_fixtures.rb | 22 ++ 7 files changed, 621 insertions(+) create mode 100644 spec/features/idv/doc_auth/socure_document_capture_spec.rb create mode 100644 spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb create mode 100644 spec/fixtures/socure_docv/pass.json create mode 100644 spec/support/socure_docv_fixtures.rb diff --git a/config/application.yml.default b/config/application.yml.default index eb65bbc102e..5a8aba29407 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -581,8 +581,10 @@ test: session_encryption_key: 27bad3c25711099429c1afdfd1890910f3b59f5a4faec1c85e945cb8b02b02f261ba501d99cfbb4fab394e0102de6fecf8ffe260f322f610db3e96b2a775c120 short_term_phone_otp_max_attempts: 100 skip_encryption_allowed_list: '[]' + socure_docv_document_request_endpoint: 'https://sandbox.socure.test/documnt-request' socure_docv_webhook_secret_key: 'secret-key' socure_docv_webhook_secret_key_queue: '["old-key-one", "old-key-two"]' + socure_idplus_base_url: 'https://sandbox.socure.test' team_ada_email: 'ada@example.com' team_all_login_emails: '["b@example.com", "c@example.com"]' team_daily_fraud_metrics_emails: '["g@example.com", "h@example.com"]' diff --git a/spec/features/idv/doc_auth/socure_document_capture_spec.rb b/spec/features/idv/doc_auth/socure_document_capture_spec.rb new file mode 100644 index 00000000000..ca3e1702bb9 --- /dev/null +++ b/spec/features/idv/doc_auth/socure_document_capture_spec.rb @@ -0,0 +1,209 @@ +require 'rails_helper' + +RSpec.feature 'document capture step', :js do + include IdvStepHelper + include DocAuthHelper + include DocCaptureHelper + include ActionView::Helpers::DateHelper + + let(:max_attempts) { 3 } + let(:fake_analytics) { FakeAnalytics.new } + let(:socure_docv_webhook_secret_key) { 'socure_docv_webhook_secret_key' } + let(:fake_socure_docv_document_request_endpoint) { 'https://fake-socure.test/document-request' } + let(:fake_socure_document_capture_app_url) { 'https://verify.fake-socure.test/something' } + + before(:each) do + allow(IdentityConfig.store).to receive(:socure_docv_enabled).and_return(true) + allow(DocAuthRouter).to receive(:doc_auth_vendor_for_bucket). + and_return(Idp::Constants::Vendors::SOCURE) + allow_any_instance_of(ServiceProviderSession).to receive(:sp_name).and_return('Test SP') + allow(IdentityConfig.store).to receive(:socure_docv_webhook_secret_key). + and_return(socure_docv_webhook_secret_key) + allow(IdentityConfig.store).to receive(:socure_docv_document_request_endpoint). + and_return(fake_socure_docv_document_request_endpoint) + allow(IdentityConfig.store).to receive(:ruby_workers_idv_enabled).and_return(false) + allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) + @docv_transaction_token = stub_docv_document_request + stub_docv_verification_data_pass + end + + before(:all) do + @user = user_with_2fa + end + + after(:all) { @user.destroy } + + context 'standard desktop flow' do + before do + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step + click_idv_continue + end + + context 'rate limits calls to backend docauth vendor', allow_browser_log: true do + before do + allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) + (max_attempts - 1).times do + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + end + end + + it 'redirects to the rate limited error page' do + expect(page).to have_current_path(fake_socure_document_capture_app_url) + visit idv_socure_document_capture_path + expect(page).to have_current_path(idv_socure_document_capture_path) + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + visit idv_socure_document_capture_path + expect(page).to have_current_path(idv_session_errors_rate_limited_path) + expect(fake_analytics).to have_logged_event( + 'Rate Limit Reached', + limiter_type: :idv_doc_auth, + ) + end + + context 'successfully processes image on last attempt' do + before do + DocAuth::Mock::DocAuthMockClient.reset! + end + + it 'proceeds to the next page with valid info' do + expect(page).to have_current_path(fake_socure_document_capture_app_url) + visit idv_socure_document_capture_path + expect(page).to have_current_path(idv_socure_document_capture_path) + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + + visit idv_socure_document_capture_update_path + expect(page).to have_current_path(idv_ssn_url) + + visit idv_socure_document_capture_path + + expect(page).to have_current_path(idv_session_errors_rate_limited_path) + end + end + end + + # ToDo post LG-14010 + context 'network connection errors' do + xit 'catches network connection errors on document request', allow_browser_log: true do + # expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) + end + + xit 'catches network connection errors on verification data request', + allow_browser_log: true do + # expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) + end + end + + it 'does not track state if state tracking is disabled' do + allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(false) + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + + expect(DocAuthLog.find_by(user_id: @user.id).state).to be_nil + end + + xit 'does track state if state tracking is disabled' do + allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(true) + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + + expect(DocAuthLog.find_by(user_id: @user.id).state).not_to be_nil + end + end + + context 'standard mobile flow' do + it 'proceeds to the next page with valid info' do + perform_in_browser(:mobile) do + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step + + expect(page).to have_current_path(idv_socure_document_capture_url) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) + click_idv_continue + socure_docv_upload_documents( + docv_transaction_token: @docv_transaction_token, + ) + visit idv_socure_document_capture_update_path + expect(page).to have_current_path(idv_ssn_url) + + expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY') + + fill_out_ssn_form_ok + click_idv_continue + complete_verify_step + expect(page).to have_current_path(idv_phone_url) + end + end + end + + def expect_rate_limited_header(expected_to_be_present) + review_issues_h1_heading = strip_tags(t('doc_auth.errors.rate_limited_heading')) + if expected_to_be_present + expect(page).to have_content(review_issues_h1_heading) + else + expect(page).not_to have_content(review_issues_h1_heading) + end + end +end + +RSpec.feature 'direct access to IPP on desktop', :js do + include IdvStepHelper + include DocAuthHelper + + context 'before handoff page' do + let(:sp_ipp_enabled) { true } + let(:in_person_proofing_opt_in_enabled) { true } + let(:facial_match_required) { false } + let(:user) { user_with_2fa } + + before do + service_provider = create(:service_provider, :active, :in_person_proofing_enabled) + allow(IdentityConfig.store).to receive(:doc_auth_selfie_desktop_test_mode).and_return(false) + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled).and_return( + in_person_proofing_opt_in_enabled, + ) + allow(IdentityConfig.store).to receive(:allowed_biometric_ial_providers). + and_return([service_provider.issuer]) + allow(IdentityConfig.store).to receive( + :allowed_valid_authn_contexts_semantic_providers, + ).and_return([service_provider.issuer]) + allow_any_instance_of(ServiceProvider).to receive(:in_person_proofing_enabled). + and_return(false) + allow(IdentityConfig.store).to receive(:socure_docv_enabled).and_return(true) + allow(DocAuthRouter).to receive(:doc_auth_vendor_for_bucket). + and_return(Idp::Constants::Vendors::SOCURE) + visit_idp_from_sp_with_ial2( + :oidc, + **{ client_id: service_provider.issuer, + facial_match_required: facial_match_required }, + ) + sign_in_via_branded_page(user) + complete_doc_auth_steps_before_agreement_step + + visit idv_socure_document_capture_path(step: 'hybrid_handoff') + end + + context 'when selfie is enabled' do + it 'redirects back to agreement page' do + expect(page).to have_current_path(idv_agreement_path) + end + end + + context 'when selfie is disabled' do + let(:facial_match_required) { false } + + it 'redirects back to agreement page' do + expect(page).to have_current_path(idv_agreement_path) + end + end + end +end diff --git a/spec/features/idv/hybrid_mobile/entry_spec.rb b/spec/features/idv/hybrid_mobile/entry_spec.rb index 7a979138551..4c43e0b5bfc 100644 --- a/spec/features/idv/hybrid_mobile/entry_spec.rb +++ b/spec/features/idv/hybrid_mobile/entry_spec.rb @@ -40,6 +40,29 @@ expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url) end end + + context 'when socure is the doc auth vendor' do + before do + allow(DocAuthRouter).to receive(:doc_auth_vendor_for_bucket). + and_return(Idp::Constants::Vendors::SOCURE) + stub_docv_document_request + end + + it 'puts the user on the socure document capture page' do + expect(link_to_visit).to be + + Capybara.using_session('mobile') do + visit link_to_visit + # Should have redirected to the actual doc capture url + expect(current_url).to eql(idv_hybrid_mobile_socure_document_capture_url) + + # Confirm that we end up on the LN / Mock page even if we try to + # go to the Socure one. + visit idv_hybrid_mobile_document_capture_url + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + end + end + end end context 'old link' do diff --git a/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb new file mode 100644 index 00000000000..d2da26e722f --- /dev/null +++ b/spec/features/idv/hybrid_mobile/hybrid_socure_mobile_spec.rb @@ -0,0 +1,255 @@ +require 'rails_helper' + +RSpec.describe 'Hybrid Flow' do + include IdvHelper + include IdvStepHelper + include DocAuthHelper + + let(:phone_number) { '415-555-0199' } + let(:sp) { :oidc } + let(:fake_socure_document_capture_app_url) { 'https://verify.fake-socure.test/something' } + let(:fake_socure_docv_document_request_endpoint) { 'https://fake-socure.test/document-request' } + + before do + allow(FeatureManagement).to receive(:doc_capture_polling_enabled?).and_return(true) + allow(IdentityConfig.store).to receive(:socure_docv_enabled).and_return(true) + allow(DocAuthRouter).to receive(:doc_auth_vendor_for_bucket). + and_return(Idp::Constants::Vendors::SOCURE) + allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) + allow(IdentityConfig.store).to receive(:ruby_workers_idv_enabled).and_return(false) + allow(IdentityConfig.store).to receive(:socure_docv_document_request_endpoint). + and_return(fake_socure_docv_document_request_endpoint) + allow(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config| + @sms_link = config[:link] + impl.call(**config) + end.at_least(1).times + @docv_transaction_token = stub_docv_document_request + end + + it 'proofs and hands off to mobile', js: true do + user = nil + + perform_in_browser(:desktop) do + visit_idp_from_sp_with_ial2(sp) + user = sign_up_and_2fa_ial1_user + + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + + expect(page).to have_content(t('doc_auth.headings.text_message')) + expect(page).to have_content(t('doc_auth.info.you_entered')) + expect(page).to have_content('+1 415-555-0199') + + # Confirm that Continue button is not shown when polling is enabled + expect(page).not_to have_content(t('doc_auth.buttons.continue')) + end + + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + + # Confirm that jumping to LinkSent page does not cause errors + visit idv_link_sent_url + expect(page).to have_current_path(root_url) + + # Confirm that we end up on the LN / Mock page even if we try to + # go to the Socure one. + visit idv_hybrid_mobile_socure_document_capture_url + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + + # Confirm that clicking cancel and then coming back doesn't cause errors + click_link 'Cancel' + visit idv_hybrid_mobile_socure_document_capture_url + + # Confirm that jumping to Phone page does not cause errors + visit idv_phone_url + expect(page).to have_current_path(root_url) + visit idv_hybrid_mobile_socure_document_capture_url + + # Confirm that jumping to Welcome page does not cause errors + visit idv_welcome_url + expect(page).to have_current_path(root_url) + visit idv_hybrid_mobile_socure_document_capture_url + + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + + stub_docv_verification_data_pass + click_idv_continue + expect(page).to have_current_path(fake_socure_document_capture_app_url) + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + visit idv_hybrid_mobile_socure_document_capture_update_url + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete'))) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) + + # To be fixed in app: + # Confirm app disallows jumping back to DocumentCapture page + # visit idv_hybrid_mobile_socure_document_capture_url + # expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + end + + perform_in_browser(:desktop) do + expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) + expect(page).to have_current_path(idv_ssn_path) + + fill_out_ssn_form_ok + click_idv_continue + + expect(page).to have_content(t('headings.verify')) + complete_verify_step + + prefilled_phone = page.find(id: 'idv_phone_form_phone').value + + expect( + PhoneFormatter.format(prefilled_phone), + ).to eq( + PhoneFormatter.format(user.default_phone_configuration.phone), + ) + + fill_out_phone_form_ok + verify_phone_otp + + fill_in t('idv.form.password'), with: Features::SessionHelper::VALID_PASSWORD + click_idv_continue + + acknowledge_and_confirm_personal_key + + validate_idv_completed_page(user) + click_agree_and_continue + + validate_return_to_sp + end + end + + it 'shows the waiting screen correctly after cancelling from mobile and restarting', js: true do + user = nil + + perform_in_browser(:desktop) do + user = sign_in_and_2fa_user + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + + expect(page).to have_content(t('doc_auth.headings.text_message')) + end + + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) + click_on t('links.cancel') + click_on t('forms.buttons.cancel') # Yes, cancel + end + + perform_in_browser(:desktop) do + expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + + expect(page).to have_content(t('doc_auth.headings.text_message')) + end + end + + context 'user is rate limited on mobile' do + let(:max_attempts) { IdentityConfig.store.doc_auth_max_attempts } + + before do + allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) + DocAuth::Mock::DocAuthMockClient.mock_response!( + method: :post_front_image, + response: DocAuth::Response.new( + success: false, + errors: { network: I18n.t('doc_auth.errors.general.network_error') }, + ), + ) + end + + it 'shows capture complete on mobile and error page on desktop', js: true do + user = nil + + perform_in_browser(:desktop) do + user = sign_in_and_2fa_user + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + + expect(page).to have_content(t('doc_auth.headings.text_message')) + end + + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + + click_idv_continue + expect(page).to have_current_path(fake_socure_document_capture_app_url) + stub_docv_verification_data_pass + max_attempts.times do + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + end + + visit idv_hybrid_mobile_socure_document_capture_update_url + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_session_errors_rate_limited_path, wait: 10) + end + end + end + + it 'prefills the phone number used on the phone step if the user has no MFA phone', :js do + user = create(:user, :with_authentication_app) + + perform_in_browser(:desktop) do + start_idv_from_sp(facial_match_required: false) + sign_in_and_2fa_user(user) + + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + end + + expect(@sms_link).to be_present + + perform_in_browser(:mobile) do + visit @sms_link + + expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url) + stub_docv_verification_data_pass + click_idv_continue + expect(page).to have_current_path(fake_socure_document_capture_app_url) + socure_docv_upload_documents(docv_transaction_token: @docv_transaction_token) + visit idv_hybrid_mobile_socure_document_capture_update_url + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_ssn_path, wait: 10) + + fill_out_ssn_form_ok + click_idv_continue + + expect(page).to have_content(t('headings.verify')) + complete_verify_step + + prefilled_phone = page.find(id: 'idv_phone_form_phone').value + + expect( + PhoneFormatter.format(prefilled_phone), + ).to eq( + PhoneFormatter.format(phone_number), + ) + end + end +end diff --git a/spec/fixtures/socure_docv/pass.json b/spec/fixtures/socure_docv/pass.json new file mode 100644 index 00000000000..d5ca2efa7f1 --- /dev/null +++ b/spec/fixtures/socure_docv/pass.json @@ -0,0 +1,41 @@ +{ + "referenceId": "a1234b56-e789-0123-4fga-56b7c890d123", + "previousReferenceId": "e9c170f2-b3e4-423b-a373-5d6e1e9b23f8", + "documentVerification": { + "reasonCodes": [ + "I831", + "R810" + ], + "documentType": { + "type": "Drivers License", + "country": "USA", + "state": "NY" + }, + "decision": { + "name": "lenient", + "value": "accept" + }, + "documentData": { + "firstName": "Dwayne", + "surName": "Denver", + "fullName": "Dwayne Denver", + "address": "123 Example Street, New York City, NY 10001", + "parsedAddress": { + "physicalAddress": "123 Example Street", + "physicalAddress2": "New York City NY 10001", + "city": "New York City", + "state": "NY", + "country": "US", + "zip": "10001" + }, + "documentNumber": "000000000", + "dob": "2002-01-01", + "issueDate": "2024-01-01", + "expirationDate": "2070-01-01" + } + }, + "customerProfile": { + "customerUserId": "129", + "userId": "u8JpWn4QsF3R7tA2" + } +} diff --git a/spec/support/features/document_capture_step_helper.rb b/spec/support/features/document_capture_step_helper.rb index 179b333acfb..f5b82332515 100644 --- a/spec/support/features/document_capture_step_helper.rb +++ b/spec/support/features/document_capture_step_helper.rb @@ -71,4 +71,73 @@ def api_image_submission_test_credential_part def click_try_again click_spinner_button_and_wait t('idv.failure.button.warning') end + + def socure_docv_upload_documents(docv_transaction_token:) + [ + 'WAITING_FOR_USER_TO_REDIRECT', + 'APP_OPENED', + 'DOCUMENT_FRONT_UPLOADED', + 'DOCUMENT_BACK_UPLOADED', + 'DOCUMENTS_UPLOADED', + 'SESSION_COMPLETE', + ].each { |event_type| socure_docv_send_webhook(docv_transaction_token:, event_type:) } + end + + def socure_docv_send_webhook( + docv_transaction_token:, + event_type: 'DOCUMENTS_UPLOADED' + ) + Faraday.post "http://#{[page.server.host, + page.server.port].join(':')}/api/webhooks/socure/event" do |req| + req.body = { + event: { + eventType: event_type, + docvTransactionToken: docv_transaction_token, + }, + }.to_json + req.headers = { + 'Content-Type': 'application/json', + Authorization: "secret #{IdentityConfig.store.socure_docv_webhook_secret_key}", + } + req.options.context = { service_name: 'socure-docv-webhook' } + end + end + + def stub_docv_verification_data_pass + stub_docv_verification_data(body: SocureDocvFixtures.pass_json) + end + + def stub_docv_verification_data(body:) + stub_request(:post, "#{IdentityConfig.store.socure_idplus_base_url}/api/3.0/EmailAuthScore"). + to_return( + headers: { + 'Content-Type' => 'application/json', + }, + body:, + ) + end + + def stub_docv_document_request( + url: 'https://verify.fake-socure.test/something', + status: 200, + token: SecureRandom.hex, + body: nil + ) + body ||= { + referenceId: 'socure-reference-id', + data: { + eventId: 'socure-event-id', + docvTransactionToken: token, + qrCode: 'qr-code', + url:, + }, + } + + stub_request(:post, IdentityConfig.store.socure_docv_document_request_endpoint). + to_return( + status:, + body: body.to_json, + ) + token + end end diff --git a/spec/support/socure_docv_fixtures.rb b/spec/support/socure_docv_fixtures.rb new file mode 100644 index 00000000000..a0e6ab756c4 --- /dev/null +++ b/spec/support/socure_docv_fixtures.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +module SocureDocvFixtures + class << self + def pass_json + raw = read_fixture_file_at_path('pass.json') + JSON.parse(raw).to_json + end + + private + + def read_fixture_file_at_path(filepath) + expanded_path = Rails.root.join( + 'spec', + 'fixtures', + 'socure_docv', + filepath, + ) + File.read(expanded_path) + end + end +end From aa2c2dbfb6624f6e31aa472e85dcec1fe4e27c41 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:20:52 -0500 Subject: [PATCH 10/23] Revert "LG-14828 Remove deprecated state ID step routes (#11510)" (#11542) This reverts commit 76219322d95c0e30ffae0827b16ab600aeb6e6c6. --- config/routes.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index b6336f12b70..0d832c5e8bf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -410,6 +410,10 @@ # sometimes underscores get messed up when linked to via SMS as: :capture_doc_dashes + # Deprecated route - temporary redirect while state id changes are rolled out + get '/in_person_proofing/state_id' => redirect('verify/in_person/state_id', status: 307) + put '/in_person_proofing/state_id' => 'in_person/state_id#update' + get '/in_person' => 'in_person#index' get '/in_person/ready_to_verify' => 'in_person/ready_to_verify#show', as: :in_person_ready_to_verify From c0eaa1d8b4c595165105647eae5b4c8d92bda9e4 Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Thu, 21 Nov 2024 16:48:10 -0800 Subject: [PATCH 11/23] Remove proofer_mock_fallback, erm, fallback (#11516) * Remove proofer_mock_fallback fallback Remove support for the proofer_mock_fallback setting when creating resolution proofers. [skip changelog] * Update ResolutionProofingJob spec Tell it to use the Instant Verify proofer for its tests. * tonight my autoformatter let me down --- ...stant_verify_residential_address_plugin.rb | 58 +++++-------------- .../instant_verify_state_id_address_plugin.rb | 58 +++++-------------- spec/jobs/resolution_proofing_job_spec.rb | 2 + ..._verify_residential_address_plugin_spec.rb | 46 +++------------ ...ant_verify_state_id_address_plugin_spec.rb | 46 +++------------ 5 files changed, 48 insertions(+), 162 deletions(-) diff --git a/app/services/proofing/resolution/plugins/instant_verify_residential_address_plugin.rb b/app/services/proofing/resolution/plugins/instant_verify_residential_address_plugin.rb index 5500b944136..85b1fb8a6eb 100644 --- a/app/services/proofing/resolution/plugins/instant_verify_residential_address_plugin.rb +++ b/app/services/proofing/resolution/plugins/instant_verify_residential_address_plugin.rb @@ -24,50 +24,22 @@ def call( end def proofer - @proofer ||= begin - # Historically, proofer_mock_fallback has controlled whether we - # use mock implementations of the Resolution and Address proofers - # (true = use mock, false = don't use mock). - # We are transitioning to a place where we will have separate - # configs for both. For the time being, we want to keep support for - # proofer_mock_fallback here. This can be removed after this code - # has been deployed and configs have been updated in all relevant - # environments. - - old_config_says_mock = IdentityConfig.store.proofer_mock_fallback - old_config_says_iv = !old_config_says_mock - new_config_says_mock = - IdentityConfig.store.idv_resolution_default_vendor == :mock - new_config_says_iv = - IdentityConfig.store.idv_resolution_default_vendor == :instant_verify - - proofer_type = - if new_config_says_mock && old_config_says_iv - # This will be the case immediately after deployment, when - # environment configs have not been updated. We need to - # fall back to the old config here. - :instant_verify - elsif new_config_says_iv - :instant_verify - else - :mock - end - - if proofer_type == :mock - Proofing::Mock::ResolutionMockClient.new - else - Proofing::LexisNexis::InstantVerify::Proofer.new( - instant_verify_workflow: IdentityConfig.store.lexisnexis_instant_verify_workflow, - account_id: IdentityConfig.store.lexisnexis_account_id, - base_url: IdentityConfig.store.lexisnexis_base_url, - username: IdentityConfig.store.lexisnexis_username, - password: IdentityConfig.store.lexisnexis_password, - hmac_key_id: IdentityConfig.store.lexisnexis_hmac_key_id, - hmac_secret_key: IdentityConfig.store.lexisnexis_hmac_secret_key, - request_mode: IdentityConfig.store.lexisnexis_request_mode, - ) + @proofer ||= + case IdentityConfig.store.idv_resolution_default_vendor + when :instant_verify + Proofing::LexisNexis::InstantVerify::Proofer.new( + instant_verify_workflow: IdentityConfig.store.lexisnexis_instant_verify_workflow, + account_id: IdentityConfig.store.lexisnexis_account_id, + base_url: IdentityConfig.store.lexisnexis_base_url, + username: IdentityConfig.store.lexisnexis_username, + password: IdentityConfig.store.lexisnexis_password, + hmac_key_id: IdentityConfig.store.lexisnexis_hmac_key_id, + hmac_secret_key: IdentityConfig.store.lexisnexis_hmac_secret_key, + request_mode: IdentityConfig.store.lexisnexis_request_mode, + ) + when :mock + Proofing::Mock::ResolutionMockClient.new end - end end def residential_address_unnecessary_result diff --git a/app/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin.rb b/app/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin.rb index b113f4bc52f..b2e1303d417 100644 --- a/app/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin.rb +++ b/app/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin.rb @@ -44,50 +44,22 @@ def call( end def proofer - @proofer ||= begin - # Historically, proofer_mock_fallback has controlled whether we - # use mock implementations of the Resolution and Address proofers - # (true = use mock, false = don't use mock). - # We are transitioning to a place where we will have separate - # configs for both. For the time being, we want to keep support for - # proofer_mock_fallback here. This can be removed after this code - # has been deployed and configs have been updated in all relevant - # environments. - - old_config_says_mock = IdentityConfig.store.proofer_mock_fallback - old_config_says_iv = !old_config_says_mock - new_config_says_mock = - IdentityConfig.store.idv_resolution_default_vendor == :mock - new_config_says_iv = - IdentityConfig.store.idv_resolution_default_vendor == :instant_verify - - proofer_type = - if new_config_says_mock && old_config_says_iv - # This will be the case immediately after deployment, when - # environment configs have not been updated. We need to - # fall back to the old config here. - :instant_verify - elsif new_config_says_iv - :instant_verify - else - :mock - end - - if proofer_type == :mock - Proofing::Mock::ResolutionMockClient.new - else - Proofing::LexisNexis::InstantVerify::Proofer.new( - instant_verify_workflow: IdentityConfig.store.lexisnexis_instant_verify_workflow, - account_id: IdentityConfig.store.lexisnexis_account_id, - base_url: IdentityConfig.store.lexisnexis_base_url, - username: IdentityConfig.store.lexisnexis_username, - password: IdentityConfig.store.lexisnexis_password, - hmac_key_id: IdentityConfig.store.lexisnexis_hmac_key_id, - hmac_secret_key: IdentityConfig.store.lexisnexis_hmac_secret_key, - request_mode: IdentityConfig.store.lexisnexis_request_mode, - ) + @proofer ||= + case IdentityConfig.store.idv_resolution_default_vendor + when :instant_verify + Proofing::LexisNexis::InstantVerify::Proofer.new( + instant_verify_workflow: IdentityConfig.store.lexisnexis_instant_verify_workflow, + account_id: IdentityConfig.store.lexisnexis_account_id, + base_url: IdentityConfig.store.lexisnexis_base_url, + username: IdentityConfig.store.lexisnexis_username, + password: IdentityConfig.store.lexisnexis_password, + hmac_key_id: IdentityConfig.store.lexisnexis_hmac_key_id, + hmac_secret_key: IdentityConfig.store.lexisnexis_hmac_secret_key, + request_mode: IdentityConfig.store.lexisnexis_request_mode, + ) + when :mock + Proofing::Mock::ResolutionMockClient.new end - end end def resolution_cannot_pass diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb index f00dafabb6e..0ce568616d6 100644 --- a/spec/jobs/resolution_proofing_job_spec.rb +++ b/spec/jobs/resolution_proofing_job_spec.rb @@ -26,6 +26,8 @@ and_return(lexisnexis_threatmetrix_mock_enabled) allow(IdentityConfig.store).to receive(:lexisnexis_threatmetrix_base_url). and_return('https://www.example.com') + allow(IdentityConfig.store).to receive(:idv_resolution_default_vendor). + and_return(:instant_verify) end describe '#perform' do diff --git a/spec/services/proofing/resolution/plugins/instant_verify_residential_address_plugin_spec.rb b/spec/services/proofing/resolution/plugins/instant_verify_residential_address_plugin_spec.rb index ed9cab23033..3f4df349359 100644 --- a/spec/services/proofing/resolution/plugins/instant_verify_residential_address_plugin_spec.rb +++ b/spec/services/proofing/resolution/plugins/instant_verify_residential_address_plugin_spec.rb @@ -133,53 +133,23 @@ def sp_cost_count_with_transaction_id subject(:proofer) { plugin.proofer } before do - allow(IdentityConfig.store).to receive(:proofer_mock_fallback). - and_return(proofer_mock_fallback) allow(IdentityConfig.store).to receive(:idv_resolution_default_vendor). and_return(idv_resolution_default_vendor) end - context 'when proofer_mock_fallback is set to true' do - let(:proofer_mock_fallback) { true } + context 'idv_resolution_default_vendor is set to :instant_verify' do + let(:idv_resolution_default_vendor) { :instant_verify } - context 'and idv_resolution_default_vendor is set to :instant_verify' do - let(:idv_resolution_default_vendor) do - :instant_verify - end - - # rubocop:disable Layout/LineLength - it 'creates an Instant Verify proofer because the new setting takes precedence over the old one when the old one is set to its default value' do - expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) - end - # rubocop:enable Layout/LineLength - end - - context 'and idv_resolution_default_vendor is set to :mock' do - let(:idv_resolution_default_vendor) { :mock } - - it 'creates a mock proofer because the two settings agree' do - expect(proofer).to be_an_instance_of(Proofing::Mock::ResolutionMockClient) - end + it 'creates an Instant Verify proofer' do + expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) end end - context 'when proofer_mock_fallback is set to false' do - let(:proofer_mock_fallback) { false } + context 'idv_resolution_default_vendor is set to :mock' do + let(:idv_resolution_default_vendor) { :mock } - context 'and idv_resolution_default_vendor is set to :instant_verify' do - let(:idv_resolution_default_vendor) { :instant_verify } - - it 'creates an Instant Verify proofer because the two settings agree' do - expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) - end - end - - context 'and idv_resolution_default_vendor is set to :mock' do - let(:idv_resolution_default_vendor) { :mock } - - it 'creates an Instant Verify proofer to support transition between configs' do - expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) - end + it 'creates a mock proofer' do + expect(proofer).to be_an_instance_of(Proofing::Mock::ResolutionMockClient) end end end diff --git a/spec/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin_spec.rb b/spec/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin_spec.rb index e395a45d64f..1b654c8f3d2 100644 --- a/spec/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin_spec.rb +++ b/spec/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin_spec.rb @@ -291,53 +291,23 @@ subject(:proofer) { plugin.proofer } before do - allow(IdentityConfig.store).to receive(:proofer_mock_fallback). - and_return(proofer_mock_fallback) allow(IdentityConfig.store).to receive(:idv_resolution_default_vendor). and_return(idv_resolution_default_vendor) end - context 'when proofer_mock_fallback is set to true' do - let(:proofer_mock_fallback) { true } + context 'idv_resolution_default_vendor is set to :instant_verify' do + let(:idv_resolution_default_vendor) { :instant_verify } - context 'and idv_resolution_default_vendor is set to :instant_verify' do - let(:idv_resolution_default_vendor) do - :instant_verify - end - - # rubocop:disable Layout/LineLength - it 'creates an Instant Verify proofer because the new setting takes precedence over the old one when the old one is set to its default value' do - expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) - end - # rubocop:enable Layout/LineLength - end - - context 'and idv_resolution_default_vendor is set to :mock' do - let(:idv_resolution_default_vendor) { :mock } - - it 'creates a mock proofer because the two settings agree' do - expect(proofer).to be_an_instance_of(Proofing::Mock::ResolutionMockClient) - end + it 'creates an Instant Verify proofer' do + expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) end end - context 'when proofer_mock_fallback is set to false' do - let(:proofer_mock_fallback) { false } + context 'idv_resolution_default_vendor is set to :mock' do + let(:idv_resolution_default_vendor) { :mock } - context 'and idv_resolution_default_vendor is set to :instant_verify' do - let(:idv_resolution_default_vendor) { :instant_verify } - - it 'creates an Instant Verify proofer because the two settings agree' do - expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) - end - end - - context 'and idv_resolution_default_vendor is set to :mock' do - let(:idv_resolution_default_vendor) { :mock } - - it 'creates an Instant Verify proofer to support transition between configs' do - expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) - end + it 'creates a mock proofer' do + expect(proofer).to be_an_instance_of(Proofing::Mock::ResolutionMockClient) end end end From 570d0c1a268258922abadb1a687843e01bd88aed Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Fri, 22 Nov 2024 12:39:46 -0800 Subject: [PATCH 12/23] LG-14725: Identity resolution via Socure (#11523) * Remove proofer_mock_fallback fallback Remove support for the proofer_mock_fallback setting when creating resolution proofers. [skip changelog] * Update ResolutionProofingJob spec Tell it to use the Instant Verify proofer for its tests. * tonight my autoformatter let me down * Add null safe navigation operator to ResolutionProofingJob If an exception happens earlier in the job, then document_capture_session can be nil here. Previously this would raise a NoMethodError, obscuring the root cause. * Rename instant_verify_state_address_result More generic, now state_address_resolution_result * Rename instant_verify_residential_address_result Go with more generic residential_address_resolution_result * RequestError -> Request::Error There were some autoloading issues in cases where calling code wants access to the Error class but does not load the Request class first * Don't crash when applicant includes extra fields In practice, applicant will have extra fields like `state_id_jurisdiction`, `state_id_number`, etc. * Allow identity resolution via Socure Update the ProgressiveProofer to be able to use Socure KYC. changelog: Upcoming Features, Identity verification, Support Socure for identity resolution. * Remove irrelevant tests * you don't have to say with_aamva everybody knows its aamva --- app/jobs/resolution_proofing_job.rb | 2 +- .../resolution/plugins/aamva_plugin.rb | 22 +- ...stant_verify_residential_address_plugin.rb | 53 --- .../plugins/residential_address_plugin.rb | 44 ++ ...s_plugin.rb => state_id_address_plugin.rb} | 37 +- .../resolution/progressive_proofer.rb | 104 ++++- .../proofing/socure/id_plus/proofer.rb | 4 +- .../proofing/socure/id_plus/request.rb | 58 +-- config/application.yml.default | 2 + lib/identity_config.rb | 8 +- spec/config/initializers/idv_config_spec.rb | 0 .../resolution/plugins/aamva_plugin_spec.rb | 22 +- ....rb => residential_address_plugin_spec.rb} | 40 +- ...pec.rb => state_id_address_plugin_spec.rb} | 82 ++-- .../resolution/progressive_proofer_spec.rb | 414 ++++++++++++------ .../proofing/socure/id_plus/proofer_spec.rb | 17 +- .../proofing/socure/id_plus/request_spec.rb | 20 +- 17 files changed, 554 insertions(+), 375 deletions(-) delete mode 100644 app/services/proofing/resolution/plugins/instant_verify_residential_address_plugin.rb create mode 100644 app/services/proofing/resolution/plugins/residential_address_plugin.rb rename app/services/proofing/resolution/plugins/{instant_verify_state_id_address_plugin.rb => state_id_address_plugin.rb} (59%) create mode 100644 spec/config/initializers/idv_config_spec.rb rename spec/services/proofing/resolution/plugins/{instant_verify_residential_address_plugin_spec.rb => residential_address_plugin_spec.rb} (79%) rename spec/services/proofing/resolution/plugins/{instant_verify_state_id_address_plugin_spec.rb => state_id_address_plugin_spec.rb} (78%) diff --git a/app/jobs/resolution_proofing_job.rb b/app/jobs/resolution_proofing_job.rb index 44401a8914e..b8cd4e08a95 100644 --- a/app/jobs/resolution_proofing_job.rb +++ b/app/jobs/resolution_proofing_job.rb @@ -76,7 +76,7 @@ def perform( if IdentityConfig.store.idv_socure_shadow_mode_enabled SocureShadowModeProofingJob.perform_later( - document_capture_session_result_id: document_capture_session.result_id, + document_capture_session_result_id: document_capture_session&.result_id, encrypted_arguments:, service_provider_issuer:, user_email: user_email_for_proofing(user), diff --git a/app/services/proofing/resolution/plugins/aamva_plugin.rb b/app/services/proofing/resolution/plugins/aamva_plugin.rb index 979bd3fd5ef..376e8b7e9a3 100644 --- a/app/services/proofing/resolution/plugins/aamva_plugin.rb +++ b/app/services/proofing/resolution/plugins/aamva_plugin.rb @@ -15,13 +15,13 @@ class AamvaPlugin def call( applicant_pii:, current_sp:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress:, timer: ) - should_proof = should_proof_state_id_with_aamva?( + should_proof = should_proof_state_id?( applicant_pii:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress:, ) @@ -85,37 +85,37 @@ def same_address_as_id?(applicant_pii) applicant_pii[:same_address_as_id].to_s == 'true' end - def should_proof_state_id_with_aamva?( + def should_proof_state_id?( applicant_pii:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress: ) return false unless aamva_supports_state_id_jurisdiction?(applicant_pii) # If the user is in in-person-proofing and they have changed their address then # they are not eligible for get-to-yes if !ipp_enrollment_in_progress || same_address_as_id?(applicant_pii) - user_can_pass_after_state_id_check?(instant_verify_state_id_address_result:) + user_can_pass_after_state_id_check?(state_id_address_resolution_result:) else - instant_verify_state_id_address_result.success? + state_id_address_resolution_result.success? end end def user_can_pass_after_state_id_check?( - instant_verify_state_id_address_result: + state_id_address_resolution_result: ) - return true if instant_verify_state_id_address_result.success? + return true if state_id_address_resolution_result.success? # For failed IV results, this method validates that the user is eligible to pass if the # failed attributes are covered by the same attributes in a successful AAMVA response # aka the Get-to-Yes w/ AAMVA feature. - if !instant_verify_state_id_address_result. + if !state_id_address_resolution_result. failed_result_can_pass_with_additional_verification? return false end attributes_aamva_can_pass = [:address, :dob, :state_id_number] attributes_requiring_additional_verification = - instant_verify_state_id_address_result.attributes_requiring_additional_verification + state_id_address_resolution_result.attributes_requiring_additional_verification results_that_cannot_pass_aamva = attributes_requiring_additional_verification - attributes_aamva_can_pass diff --git a/app/services/proofing/resolution/plugins/instant_verify_residential_address_plugin.rb b/app/services/proofing/resolution/plugins/instant_verify_residential_address_plugin.rb deleted file mode 100644 index 85b1fb8a6eb..00000000000 --- a/app/services/proofing/resolution/plugins/instant_verify_residential_address_plugin.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -module Proofing - module Resolution - module Plugins - class InstantVerifyResidentialAddressPlugin - def call( - applicant_pii:, - current_sp:, - ipp_enrollment_in_progress:, - timer: - ) - return residential_address_unnecessary_result unless ipp_enrollment_in_progress - - timer.time('residential address') do - proofer.proof(applicant_pii) - end.tap do |result| - Db::SpCost::AddSpCost.call( - current_sp, - :lexis_nexis_resolution, - transaction_id: result.transaction_id, - ) - end - end - - def proofer - @proofer ||= - case IdentityConfig.store.idv_resolution_default_vendor - when :instant_verify - Proofing::LexisNexis::InstantVerify::Proofer.new( - instant_verify_workflow: IdentityConfig.store.lexisnexis_instant_verify_workflow, - account_id: IdentityConfig.store.lexisnexis_account_id, - base_url: IdentityConfig.store.lexisnexis_base_url, - username: IdentityConfig.store.lexisnexis_username, - password: IdentityConfig.store.lexisnexis_password, - hmac_key_id: IdentityConfig.store.lexisnexis_hmac_key_id, - hmac_secret_key: IdentityConfig.store.lexisnexis_hmac_secret_key, - request_mode: IdentityConfig.store.lexisnexis_request_mode, - ) - when :mock - Proofing::Mock::ResolutionMockClient.new - end - end - - def residential_address_unnecessary_result - Proofing::Resolution::Result.new( - success: true, errors: {}, exception: nil, vendor_name: 'ResidentialAddressNotRequired', - ) - end - end - end - end -end diff --git a/app/services/proofing/resolution/plugins/residential_address_plugin.rb b/app/services/proofing/resolution/plugins/residential_address_plugin.rb new file mode 100644 index 00000000000..4ad356b86ba --- /dev/null +++ b/app/services/proofing/resolution/plugins/residential_address_plugin.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Proofing + module Resolution + module Plugins + class ResidentialAddressPlugin + attr_reader :proofer, :sp_cost_token + + def initialize( + proofer:, + sp_cost_token: + ) + @proofer = proofer + @sp_cost_token = sp_cost_token + end + + def call( + applicant_pii:, + current_sp:, + ipp_enrollment_in_progress:, + timer: + ) + return residential_address_unnecessary_result unless ipp_enrollment_in_progress + + timer.time('residential address') do + proofer.proof(applicant_pii) + end.tap do |result| + Db::SpCost::AddSpCost.call( + current_sp, + :lexis_nexis_resolution, + transaction_id: result.transaction_id, + ) + end + end + + def residential_address_unnecessary_result + Proofing::Resolution::Result.new( + success: true, errors: {}, exception: nil, vendor_name: 'ResidentialAddressNotRequired', + ) + end + end + end + end +end diff --git a/app/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin.rb b/app/services/proofing/resolution/plugins/state_id_address_plugin.rb similarity index 59% rename from app/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin.rb rename to app/services/proofing/resolution/plugins/state_id_address_plugin.rb index b2e1303d417..59768fbf28d 100644 --- a/app/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin.rb +++ b/app/services/proofing/resolution/plugins/state_id_address_plugin.rb @@ -3,7 +3,9 @@ module Proofing module Resolution module Plugins - class InstantVerifyStateIdAddressPlugin + class StateIdAddressPlugin + attr_reader :proofer, :sp_cost_token + SECONDARY_ID_ADDRESS_MAP = { identity_doc_address1: :address1, identity_doc_address2: :address2, @@ -12,18 +14,26 @@ class InstantVerifyStateIdAddressPlugin identity_doc_zipcode: :zipcode, }.freeze + def initialize( + proofer:, + sp_cost_token: + ) + @proofer = proofer + @sp_cost_token = sp_cost_token + end + def call( applicant_pii:, current_sp:, - instant_verify_residential_address_result:, + residential_address_resolution_result:, ipp_enrollment_in_progress:, timer: ) if same_address_as_id?(applicant_pii) && ipp_enrollment_in_progress - return instant_verify_residential_address_result + return residential_address_resolution_result end - return resolution_cannot_pass unless instant_verify_residential_address_result.success? + return resolution_cannot_pass unless residential_address_resolution_result.success? applicant_pii_with_state_id_address = if ipp_enrollment_in_progress @@ -43,25 +53,6 @@ def call( end end - def proofer - @proofer ||= - case IdentityConfig.store.idv_resolution_default_vendor - when :instant_verify - Proofing::LexisNexis::InstantVerify::Proofer.new( - instant_verify_workflow: IdentityConfig.store.lexisnexis_instant_verify_workflow, - account_id: IdentityConfig.store.lexisnexis_account_id, - base_url: IdentityConfig.store.lexisnexis_base_url, - username: IdentityConfig.store.lexisnexis_username, - password: IdentityConfig.store.lexisnexis_password, - hmac_key_id: IdentityConfig.store.lexisnexis_hmac_key_id, - hmac_secret_key: IdentityConfig.store.lexisnexis_hmac_secret_key, - request_mode: IdentityConfig.store.lexisnexis_request_mode, - ) - when :mock - Proofing::Mock::ResolutionMockClient.new - end - end - def resolution_cannot_pass Proofing::Resolution::Result.new( success: false, errors: {}, exception: nil, vendor_name: 'ResolutionCannotPass', diff --git a/app/services/proofing/resolution/progressive_proofer.rb b/app/services/proofing/resolution/progressive_proofer.rb index 757a4f244e9..2fbe8c9c3bd 100644 --- a/app/services/proofing/resolution/progressive_proofer.rb +++ b/app/services/proofing/resolution/progressive_proofer.rb @@ -8,17 +8,19 @@ module Resolution # 2. The user has only provided one address for their residential and identity document # address or separate residential and identity document addresses class ProgressiveProofer + class InvalidProofingVendorError; end + attr_reader :aamva_plugin, - :instant_verify_residential_address_plugin, - :instant_verify_state_id_address_plugin, :threatmetrix_plugin + PROOFING_VENDOR_SP_COST_TOKENS = { + mock: :mock_resolution, + instant_verify: :lexis_nexis_resolution, + socure_kyc: :socure_resolution, + }.freeze + def initialize @aamva_plugin = Plugins::AamvaPlugin.new - @instant_verify_residential_address_plugin = - Plugins::InstantVerifyResidentialAddressPlugin.new - @instant_verify_state_id_address_plugin = - Plugins::InstantVerifyStateIdAddressPlugin.new @threatmetrix_plugin = Plugins::ThreatMetrixPlugin.new end @@ -50,17 +52,17 @@ def proof( user_email:, ) - instant_verify_residential_address_result = instant_verify_residential_address_plugin.call( + residential_address_resolution_result = residential_address_plugin.call( applicant_pii:, current_sp:, ipp_enrollment_in_progress:, timer:, ) - instant_verify_state_id_address_result = instant_verify_state_id_address_plugin.call( + state_id_address_resolution_result = state_id_address_plugin.call( applicant_pii:, current_sp:, - instant_verify_residential_address_result:, + residential_address_resolution_result:, ipp_enrollment_in_progress:, timer:, ) @@ -68,7 +70,7 @@ def proof( state_id_result = aamva_plugin.call( applicant_pii:, current_sp:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress:, timer:, ) @@ -76,14 +78,92 @@ def proof( ResultAdjudicator.new( device_profiling_result: device_profiling_result, ipp_enrollment_in_progress: ipp_enrollment_in_progress, - resolution_result: instant_verify_state_id_address_result, + resolution_result: state_id_address_resolution_result, should_proof_state_id: aamva_plugin.aamva_supports_state_id_jurisdiction?(applicant_pii), state_id_result: state_id_result, - residential_resolution_result: instant_verify_residential_address_result, + residential_resolution_result: residential_address_resolution_result, same_address_as_id: applicant_pii[:same_address_as_id], applicant_pii: applicant_pii, ) end + + def proofing_vendor + @proofing_vendor ||= begin + default_vendor = IdentityConfig.store.idv_resolution_default_vendor + alternate_vendor = IdentityConfig.store.idv_resolution_alternate_vendor + alternate_vendor_percent = IdentityConfig.store.idv_resolution_alternate_vendor_percent + + if alternate_vendor == :none + return default_vendor + end + + if (rand * 100) <= alternate_vendor_percent + alternate_vendor + else + default_vendor + end + end + end + + def residential_address_plugin + @residential_address_plugin ||= Plugins::ResidentialAddressPlugin.new( + proofer: create_proofer, + sp_cost_token:, + ) + end + + def state_id_address_plugin + @state_id_address_plugin ||= Plugins::StateIdAddressPlugin.new( + proofer: create_proofer, + sp_cost_token:, + ) + end + + def create_proofer + case proofing_vendor + when :instant_verify then create_instant_verify_proofer + when :mock then create_mock_proofer + when :socure_kyc then create_socure_proofer + else + raise InvalidProofingVendorError, "#{proofing_vendor} is not a valid proofing vendor" + end + end + + def create_instant_verify_proofer + Proofing::LexisNexis::InstantVerify::Proofer.new( + instant_verify_workflow: IdentityConfig.store.lexisnexis_instant_verify_workflow, + account_id: IdentityConfig.store.lexisnexis_account_id, + base_url: IdentityConfig.store.lexisnexis_base_url, + username: IdentityConfig.store.lexisnexis_username, + password: IdentityConfig.store.lexisnexis_password, + hmac_key_id: IdentityConfig.store.lexisnexis_hmac_key_id, + hmac_secret_key: IdentityConfig.store.lexisnexis_hmac_secret_key, + request_mode: IdentityConfig.store.lexisnexis_request_mode, + ) + end + + def create_mock_proofer + Proofing::Mock::ResolutionMockClient.new + end + + def create_socure_proofer + Proofing::Socure::IdPlus::Proofer.new( + Proofing::Socure::IdPlus::Config.new( + api_key: IdentityConfig.store.socure_idplus_api_key, + base_url: IdentityConfig.store.socure_idplus_base_url, + timeout: IdentityConfig.store.socure_idplus_timeout_in_seconds, + ), + ) + end + + def sp_cost_token + PROOFING_VENDOR_SP_COST_TOKENS[proofing_vendor].tap do |token| + if !token.present? + raise InvalidProofingVendorError, + "No cost token present for proofing vendor #{proofing_vendor}" + end + end + end end end end diff --git a/app/services/proofing/socure/id_plus/proofer.rb b/app/services/proofing/socure/id_plus/proofer.rb index 4184fed409d..2bbb2ef7b14 100644 --- a/app/services/proofing/socure/id_plus/proofer.rb +++ b/app/services/proofing/socure/id_plus/proofer.rb @@ -34,14 +34,14 @@ def initialize(config) # @param [Hash] applicant # @return [Proofing::Resolution::Result] def proof(applicant) - input = Input.new(applicant.except(:phone_source)) + input = Input.new(applicant.slice(*Input.members)) request = Request.new(config:, input:) response = request.send_request build_result_from_response(response) - rescue Proofing::TimeoutError, RequestError => err + rescue Proofing::TimeoutError, Request::Error => err build_result_from_error(err) end diff --git a/app/services/proofing/socure/id_plus/request.rb b/app/services/proofing/socure/id_plus/request.rb index 881c495d63d..74b380ffe0b 100644 --- a/app/services/proofing/socure/id_plus/request.rb +++ b/app/services/proofing/socure/id_plus/request.rb @@ -3,42 +3,42 @@ module Proofing module Socure module IdPlus - class RequestError < StandardError - def initialize(wrapped) - @wrapped = wrapped - super(build_message) - end + class Request + class Error < StandardError + def initialize(wrapped) + @wrapped = wrapped + super(build_message) + end - def reference_id - return @reference_id if defined?(@reference_id) - @reference_id = response_body.is_a?(Hash) ? - response_body['referenceId'] : - nil - end + def reference_id + return @reference_id if defined?(@reference_id) + @reference_id = response_body.is_a?(Hash) ? + response_body['referenceId'] : + nil + end - def response_body - return @response_body if defined?(@response_body) - @response_body = wrapped.try(:response_body) - end + def response_body + return @response_body if defined?(@response_body) + @response_body = wrapped.try(:response_body) + end - def response_status - return @response_status if defined?(@response_status) - @response_status = wrapped.try(:response_status) - end + def response_status + return @response_status if defined?(@response_status) + @response_status = wrapped.try(:response_status) + end - private + private - attr_reader :wrapped + attr_reader :wrapped - def build_message - message = response_body.is_a?(Hash) ? response_body['msg'] : nil - message ||= wrapped.message - status = response_status ? " (#{response_status})" : '' - [message, status].join('') + def build_message + message = response_body.is_a?(Hash) ? response_body['msg'] : nil + message ||= wrapped.message + status = response_status ? " (#{response_status})" : '' + [message, status].join('') + end end - end - class Request attr_reader :config, :input SERVICE_NAME = 'socure_id_plus' @@ -78,7 +78,7 @@ def send_request 'Timed out waiting for verification response' end - raise RequestError, e + raise Error, e end def body diff --git a/config/application.yml.default b/config/application.yml.default index 5a8aba29407..3e72fd32c6f 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -156,6 +156,8 @@ idv_available: true idv_contact_phone_number: (844) 555-5555 idv_max_attempts: 5 idv_min_age_years: 13 +idv_resolution_alternate_vendor: none +idv_resolution_alternate_vendor_percent: 0 idv_resolution_default_vendor: mock idv_send_link_attempt_window_in_minutes: 10 idv_send_link_max_attempts: 5 diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 44938a1fc13..bf04825a906 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -174,10 +174,16 @@ def self.store config.add(:idv_contact_phone_number, type: :string) config.add(:idv_max_attempts, type: :integer) config.add(:idv_min_age_years, type: :integer) + config.add( + :idv_resolution_alternate_vendor, + type: :symbol, + enum: [:instant_verify, :socure_kyc, :mock, :none], + ) + config.add(:idv_resolution_alternate_vendor_percent, type: :integer) config.add( :idv_resolution_default_vendor, type: :symbol, - enum: [:instant_verify, :mock], + enum: [:instant_verify, :socure_kyc, :mock], ) config.add(:idv_send_link_attempt_window_in_minutes, type: :integer) config.add(:idv_send_link_max_attempts, type: :integer) diff --git a/spec/config/initializers/idv_config_spec.rb b/spec/config/initializers/idv_config_spec.rb new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spec/services/proofing/resolution/plugins/aamva_plugin_spec.rb b/spec/services/proofing/resolution/plugins/aamva_plugin_spec.rb index cfd24e0c754..f23c51056c4 100644 --- a/spec/services/proofing/resolution/plugins/aamva_plugin_spec.rb +++ b/spec/services/proofing/resolution/plugins/aamva_plugin_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Proofing::Resolution::Plugins::AamvaPlugin do let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN } let(:current_sp) { build(:service_provider) } - let(:instant_verify_state_id_address_result) { nil } + let(:state_id_address_resolution_result) { nil } let(:ipp_enrollment_in_progress) { false } let(:proofer) { instance_double(Proofing::Aamva::Proofer, proof: proofer_result) } let(:proofer_result) do @@ -40,7 +40,7 @@ def sp_cost_count_with_transaction_id plugin.call( applicant_pii:, current_sp:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress:, timer: JobHelpers::Timer.new, ) @@ -60,7 +60,7 @@ def sp_cost_count_with_transaction_id end context 'InstantVerify succeeded' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: true, vendor_name: 'lexisnexis:instant_verify', @@ -96,7 +96,7 @@ def sp_cost_count_with_transaction_id context 'InstantVerify failed' do context 'and the failure can possibly be covered by AAMVA' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: false, vendor_name: 'lexisnexis:instant_verify', @@ -120,7 +120,7 @@ def sp_cost_count_with_transaction_id end context 'but the failure cannot be covered by AAMVA' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: false, vendor_name: 'lexisnexis:instant_verify', @@ -164,7 +164,7 @@ def sp_cost_count_with_transaction_id context 'residential address same as id address' do let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID } - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: true, vendor_name: 'lexisnexis:instant_verify', @@ -184,7 +184,7 @@ def sp_cost_count_with_transaction_id context 'InstantVerify failed' do context 'and the failure can possibly be covered by AAMVA' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: false, vendor_name: 'lexisnexis:instant_verify', @@ -204,7 +204,7 @@ def sp_cost_count_with_transaction_id end context 'but the failure cannot be covered by AAMVA' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: false, vendor_name: 'lexisnexis:instant_verify', @@ -236,7 +236,7 @@ def sp_cost_count_with_transaction_id context 'InstantVerify succeeded for residential address' do context 'and InstantVerify passed for id address' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: true, vendor_name: 'lexisnexis:instant_verify', @@ -255,7 +255,7 @@ def sp_cost_count_with_transaction_id context 'and InstantVerify failed for state id address' do context 'but the failure can possibly be covered by AAMVA' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: false, vendor_name: 'lexisnexis:instant_verify', @@ -275,7 +275,7 @@ def sp_cost_count_with_transaction_id end context 'and the failure cannot be covered by AAMVA' do - let(:instant_verify_state_id_address_result) do + let(:state_id_address_resolution_result) do Proofing::Resolution::Result.new( success: false, vendor_name: 'lexisnexis:instant_verify', diff --git a/spec/services/proofing/resolution/plugins/instant_verify_residential_address_plugin_spec.rb b/spec/services/proofing/resolution/plugins/residential_address_plugin_spec.rb similarity index 79% rename from spec/services/proofing/resolution/plugins/instant_verify_residential_address_plugin_spec.rb rename to spec/services/proofing/resolution/plugins/residential_address_plugin_spec.rb index 3f4df349359..e8af3647ee9 100644 --- a/spec/services/proofing/resolution/plugins/instant_verify_residential_address_plugin_spec.rb +++ b/spec/services/proofing/resolution/plugins/residential_address_plugin_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe Proofing::Resolution::Plugins::InstantVerifyResidentialAddressPlugin do +RSpec.describe Proofing::Resolution::Plugins::ResidentialAddressPlugin do let(:current_sp) { build(:service_provider) } let(:ipp_enrollment_in_progress) { false } @@ -11,12 +11,21 @@ Proofing::Resolution::Result.new( success: true, transaction_id: proofer_transaction_id, - vendor_name: 'lexisnexis:instant_verify', + vendor_name: 'test_resolution_vendor', ) end + let(:proofer) do + instance_double(Proofing::LexisNexis::InstantVerify::Proofer, proof: proofer_result) + end + + let(:sp_cost_token) { :test_cost_token } + subject(:plugin) do - described_class.new + described_class.new( + proofer:, + sp_cost_token:, + ) end describe '#call' do @@ -128,29 +137,4 @@ def sp_cost_count_with_transaction_id end end end - - describe '#proofer' do - subject(:proofer) { plugin.proofer } - - before do - allow(IdentityConfig.store).to receive(:idv_resolution_default_vendor). - and_return(idv_resolution_default_vendor) - end - - context 'idv_resolution_default_vendor is set to :instant_verify' do - let(:idv_resolution_default_vendor) { :instant_verify } - - it 'creates an Instant Verify proofer' do - expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) - end - end - - context 'idv_resolution_default_vendor is set to :mock' do - let(:idv_resolution_default_vendor) { :mock } - - it 'creates a mock proofer' do - expect(proofer).to be_an_instance_of(Proofing::Mock::ResolutionMockClient) - end - end - end end diff --git a/spec/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin_spec.rb b/spec/services/proofing/resolution/plugins/state_id_address_plugin_spec.rb similarity index 78% rename from spec/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin_spec.rb rename to spec/services/proofing/resolution/plugins/state_id_address_plugin_spec.rb index 1b654c8f3d2..de3ed2161bc 100644 --- a/spec/services/proofing/resolution/plugins/instant_verify_state_id_address_plugin_spec.rb +++ b/spec/services/proofing/resolution/plugins/state_id_address_plugin_spec.rb @@ -1,16 +1,16 @@ require 'rails_helper' -RSpec.describe Proofing::Resolution::Plugins::InstantVerifyStateIdAddressPlugin do +RSpec.describe Proofing::Resolution::Plugins::StateIdAddressPlugin do let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID } let(:current_sp) { build(:service_provider) } - let(:instant_verify_residential_address_result) do + let(:residential_address_resolution_result) do Proofing::Resolution::Result.new( success: true, errors: {}, exception: nil, - vendor_name: 'lexisnexis:instant_verify', + vendor_name: 'test_resolution_vendor', ) end @@ -21,12 +21,21 @@ success: true, errors: {}, exception: nil, - vendor_name: 'lexisnexis:instant_verify', + vendor_name: 'test_resolution_vendor', ) end + let(:proofer) do + instance_double(Proofing::LexisNexis::InstantVerify::Proofer, proof: proofer_result) + end + + let(:sp_cost_token) { :test_cost_token } + subject(:plugin) do - described_class.new + described_class.new( + proofer:, + sp_cost_token:, + ) end describe '#call' do @@ -35,15 +44,11 @@ applicant_pii:, current_sp:, ipp_enrollment_in_progress:, - instant_verify_residential_address_result:, + residential_address_resolution_result:, timer: JobHelpers::Timer.new, ) end - before do - allow(plugin.proofer).to receive(:proof).and_return(proofer_result) - end - context 'remote unsupervised proofing' do let(:ipp_enrollment_in_progress) { false } @@ -61,18 +66,17 @@ it 'passes state id address to proofer' do expect(plugin.proofer). to receive(:proof). - with(hash_including(state_id_address)). - and_call_original + with(hash_including(state_id_address)) call end - context 'when InstantVerify call succeeds' do + context 'when vendor call succeeds' do it 'returns the proofer result' do expect(call).to eql(proofer_result) end - it 'records a LexisNexis SP cost' do + it 'records correct SP cost' do expect { call }. to change { SpCost.where( @@ -83,13 +87,13 @@ end end - context 'when InstantVerify call fails' do + context 'when vendor call fails' do let(:proofer_result) do Proofing::Resolution::Result.new( success: false, errors: {}, exception: nil, - vendor_name: 'lexisnexis:instant_verify', + vendor_name: 'test_resolution_vendor', ) end @@ -108,7 +112,7 @@ end end - context 'when InstantVerify call results in exception' do + context 'when vendor call results in exception' do let(:proofer_result) do Proofing::Resolution::Result.new( success: false, @@ -139,7 +143,7 @@ it 'reuses residential address result' do result = call expect(plugin.proofer).not_to have_received(:proof) - expect(result).to eql(instant_verify_residential_address_result) + expect(result).to eql(residential_address_resolution_result) end it 'does not add a new LexisNexis SP cost (since residential address result was reused)' do @@ -176,15 +180,14 @@ } end - context 'LexisNexis InstantVerify passes for residential address' do - it 'calls the InstantVerify Proofer with state id address' do - expect(plugin.proofer).to receive(:proof).with(hash_including(state_id_address)). - and_call_original + context 'LexisNexis vendor passes for residential address' do + it 'calls the vendor Proofer with state id address' do + expect(plugin.proofer).to receive(:proof).with(hash_including(state_id_address)) call end - context 'when InstantVerify call succeeds' do + context 'when vendor call succeeds' do it 'returns the proofer result' do expect(call).to eql(proofer_result) end @@ -200,7 +203,7 @@ end end - context 'when InstantVerify call fails' do + context 'when vendor call fails' do let(:proofer_result) do Proofing::Resolution::Result.new( success: false, @@ -225,7 +228,7 @@ end end - context 'when InstantVerify call results in exception' do + context 'when vendor call results in exception' do let(:proofer_result) do Proofing::Resolution::Result.new( success: false, @@ -250,8 +253,8 @@ end end - context 'LexisNexis InstantVerify failed for residential address' do - let(:instant_verify_residential_address_result) do + context 'LexisNexis vendor failed for residential address' do + let(:residential_address_resolution_result) do Proofing::Resolution::Result.new( success: false, errors: {}, @@ -286,29 +289,4 @@ end end end - - describe '#proofer' do - subject(:proofer) { plugin.proofer } - - before do - allow(IdentityConfig.store).to receive(:idv_resolution_default_vendor). - and_return(idv_resolution_default_vendor) - end - - context 'idv_resolution_default_vendor is set to :instant_verify' do - let(:idv_resolution_default_vendor) { :instant_verify } - - it 'creates an Instant Verify proofer' do - expect(proofer).to be_an_instance_of(Proofing::LexisNexis::InstantVerify::Proofer) - end - end - - context 'idv_resolution_default_vendor is set to :mock' do - let(:idv_resolution_default_vendor) { :mock } - - it 'creates a mock proofer' do - expect(proofer).to be_an_instance_of(Proofing::Mock::ResolutionMockClient) - end - end - end end diff --git a/spec/services/proofing/resolution/progressive_proofer_spec.rb b/spec/services/proofing/resolution/progressive_proofer_spec.rb index 10723e687da..5b7eab4a20a 100644 --- a/spec/services/proofing/resolution/progressive_proofer_spec.rb +++ b/spec/services/proofing/resolution/progressive_proofer_spec.rb @@ -1,116 +1,14 @@ require 'rails_helper' RSpec.describe Proofing::Resolution::ProgressiveProofer do - let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN } - let(:ipp_enrollment_in_progress) { false } - let(:request_ip) { Faker::Internet.ip_v4_address } - let(:threatmetrix_session_id) { SecureRandom.uuid } - let(:user_email) { Faker::Internet.email } - let(:current_sp) { build(:service_provider) } - - let(:instant_verify_residential_address_plugin) do - Proofing::Resolution::Plugins::InstantVerifyResidentialAddressPlugin.new - end - - let(:instant_verify_residential_address_result) do - Proofing::Resolution::Result.new( - success: true, - transaction_id: 'iv-residential', - ) - end - - let(:instant_verify_residential_address_proofer) do - instance_double( - Proofing::LexisNexis::InstantVerify::Proofer, - proof: instant_verify_residential_address_result, - ) - end - - let(:instant_verify_state_id_address_plugin) do - Proofing::Resolution::Plugins::InstantVerifyStateIdAddressPlugin.new - end - - let(:instant_verify_state_id_address_result) do - Proofing::Resolution::Result.new( - success: true, - transaction_id: 'iv-state-id', - ) - end - - let(:instant_verify_state_id_address_proofer) do - instance_double( - Proofing::LexisNexis::InstantVerify::Proofer, - proof: instant_verify_state_id_address_result, - ) - end - - let(:aamva_plugin) { Proofing::Resolution::Plugins::AamvaPlugin.new } - - let(:aamva_result) do - Proofing::StateIdResult.new( - success: false, - transaction_id: 'aamva-123', - ) - end - - let(:aamva_proofer) { instance_double(Proofing::Aamva::Proofer, proof: aamva_result) } - - let(:threatmetrix_plugin) do - Proofing::Resolution::Plugins::ThreatMetrixPlugin.new - end - - let(:threatmetrix_result) do - Proofing::DdpResult.new( - success: true, - transaction_id: 'ddp-123', - ) - end - - let(:threatmetrix_proofer) do - instance_double( - Proofing::LexisNexis::Ddp::Proofer, - proof: threatmetrix_result, - ) - end - subject(:progressive_proofer) { described_class.new } - before do - allow(progressive_proofer).to receive(:threatmetrix_plugin).and_return(threatmetrix_plugin) - allow(threatmetrix_plugin).to receive(:proofer).and_return(threatmetrix_proofer) - - allow(progressive_proofer).to receive(:aamva_plugin).and_return(aamva_plugin) - allow(aamva_plugin).to receive(:proofer).and_return(aamva_proofer) - - allow(progressive_proofer).to receive(:instant_verify_residential_address_plugin). - and_return(instant_verify_residential_address_plugin) - allow(instant_verify_residential_address_plugin).to receive(:proofer). - and_return(instant_verify_residential_address_proofer) - - allow(progressive_proofer).to receive(:instant_verify_state_id_address_plugin). - and_return(instant_verify_state_id_address_plugin) - allow(instant_verify_state_id_address_plugin).to receive(:proofer). - and_return(instant_verify_state_id_address_proofer) - end - it 'assigns aamva_plugin' do expect(described_class.new.aamva_plugin).to be_a( Proofing::Resolution::Plugins::AamvaPlugin, ) end - it 'assigns instant_verify_residential_address_plugin' do - expect(described_class.new.instant_verify_residential_address_plugin).to be_a( - Proofing::Resolution::Plugins::InstantVerifyResidentialAddressPlugin, - ) - end - - it 'assigns instant_verify_state_id_address_plugin' do - expect(described_class.new.instant_verify_state_id_address_plugin).to be_a( - Proofing::Resolution::Plugins::InstantVerifyStateIdAddressPlugin, - ) - end - it 'assigns threatmetrix_plugin' do expect(described_class.new.threatmetrix_plugin).to be_a( Proofing::Resolution::Plugins::ThreatMetrixPlugin, @@ -118,6 +16,62 @@ end describe '#proof' do + let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN } + let(:ipp_enrollment_in_progress) { false } + let(:request_ip) { Faker::Internet.ip_v4_address } + let(:threatmetrix_session_id) { SecureRandom.uuid } + let(:user_email) { Faker::Internet.email } + let(:current_sp) { build(:service_provider) } + + let(:residential_address_resolution_result) do + Proofing::Resolution::Result.new( + success: true, + transaction_id: 'residential-resolution-tx', + ) + end + + let(:state_id_address_resolution_result) do + Proofing::Resolution::Result.new( + success: true, + transaction_id: 'state-id-resolution-tx', + ) + end + + let(:resolution_proofing_results) do + # In cases where both calls are made, the residential call is made + # before the state id address call + [residential_address_resolution_result, state_id_address_resolution_result] + end + + let(:resolution_proofer) do + instance_double( + Proofing::LexisNexis::InstantVerify::Proofer, + ) + end + + let(:aamva_result) do + Proofing::StateIdResult.new( + success: false, + transaction_id: 'aamva-123', + ) + end + + let(:aamva_proofer) { instance_double(Proofing::Aamva::Proofer, proof: aamva_result) } + + let(:threatmetrix_result) do + Proofing::DdpResult.new( + success: true, + transaction_id: 'ddp-123', + ) + end + + let(:threatmetrix_proofer) do + instance_double( + Proofing::LexisNexis::Ddp::Proofer, + proof: threatmetrix_result, + ) + end + subject(:proof) do progressive_proofer.proof( applicant_pii:, @@ -130,20 +84,37 @@ ) end + before do + allow(resolution_proofer).to receive(:proof).and_return(*resolution_proofing_results) + allow(progressive_proofer).to receive(:create_proofer). + and_return(resolution_proofer) + + allow(progressive_proofer.threatmetrix_plugin).to receive(:proofer). + and_return(threatmetrix_proofer) + + allow(progressive_proofer.aamva_plugin).to receive(:proofer). + and_return(aamva_proofer) + end + context 'remote unsupervised proofing' do + let(:resolution_proofing_results) do + # No call is made for residential address on remote unsupervised path + [state_id_address_resolution_result] + end + it 'calls AamvaPlugin' do - expect(aamva_plugin).to receive(:call).with( + expect(progressive_proofer.aamva_plugin).to receive(:call).with( applicant_pii:, current_sp:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress: false, timer: an_instance_of(JobHelpers::Timer), ) proof end - it 'calls InstantVerifyResidentialAddressPlugin' do - expect(instant_verify_residential_address_plugin).to receive(:call).with( + it 'calls ResidentialAddressPlugin' do + expect(progressive_proofer.residential_address_plugin).to receive(:call).with( applicant_pii:, current_sp:, ipp_enrollment_in_progress: false, @@ -152,11 +123,11 @@ proof end - it 'calls InstantVerifyStateIdAddressPlugin' do - expect(instant_verify_state_id_address_plugin).to receive(:call).with( + it 'calls StateIdAddressPlugin' do + expect(progressive_proofer.state_id_address_plugin).to receive(:call).with( applicant_pii:, current_sp:, - instant_verify_residential_address_result: satisfy do |result| + residential_address_resolution_result: satisfy do |result| expect(result.success?).to eql(true) expect(result.vendor_name).to eql('ResidentialAddressNotRequired') end, @@ -167,7 +138,7 @@ end it 'calls ThreatMetrixPlugin' do - expect(threatmetrix_plugin).to receive(:call).with( + expect(progressive_proofer.threatmetrix_plugin).to receive(:call).with( applicant_pii:, current_sp:, request_ip:, @@ -182,7 +153,7 @@ proof.tap do |result| expect(result).to be_an_instance_of(Proofing::Resolution::ResultAdjudicator) - expect(result.resolution_result).to eql(instant_verify_state_id_address_result) + expect(result.resolution_result).to eql(state_id_address_resolution_result) expect(result.state_id_result).to eql(aamva_result) expect(result.device_profiling_result).to eql(threatmetrix_result) expect(result.residential_resolution_result).to satisfy do |result| @@ -201,15 +172,15 @@ context 'residential address is same as id' do let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID } - let(:instant_verify_state_id_address_result) do - instant_verify_residential_address_result + let(:state_id_address_resolution_result) do + residential_address_resolution_result end it 'calls AamvaPlugin' do - expect(aamva_plugin).to receive(:call).with( + expect(progressive_proofer.aamva_plugin).to receive(:call).with( applicant_pii:, current_sp:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress: true, timer: an_instance_of(JobHelpers::Timer), ) @@ -217,8 +188,8 @@ proof end - it 'calls InstantVerifyResidentialAddressPlugin' do - expect(instant_verify_residential_address_plugin).to receive(:call).with( + it 'calls ResidentialAddressPlugin' do + expect(progressive_proofer.residential_address_plugin).to receive(:call).with( applicant_pii:, current_sp:, ipp_enrollment_in_progress: true, @@ -227,11 +198,11 @@ proof end - it 'calls InstantVerifyStateIdAddressPlugin' do - expect(instant_verify_state_id_address_plugin).to receive(:call).with( + it 'calls StateIdAddressPlugin' do + expect(progressive_proofer.state_id_address_plugin).to receive(:call).with( applicant_pii:, current_sp:, - instant_verify_residential_address_result:, + residential_address_resolution_result:, ipp_enrollment_in_progress: true, timer: an_instance_of(JobHelpers::Timer), ).and_call_original @@ -239,7 +210,7 @@ end it 'calls ThreatMetrixPlugin' do - expect(threatmetrix_plugin).to receive(:call).with( + expect(progressive_proofer.threatmetrix_plugin).to receive(:call).with( applicant_pii:, current_sp:, request_ip:, @@ -254,11 +225,11 @@ proof.tap do |result| expect(result).to be_an_instance_of(Proofing::Resolution::ResultAdjudicator) - expect(result.resolution_result).to eql(instant_verify_state_id_address_result) + expect(result.resolution_result).to eql(state_id_address_resolution_result) expect(result.state_id_result).to eql(aamva_result) expect(result.device_profiling_result).to eql(threatmetrix_result) expect(result.residential_resolution_result).to( - eql(instant_verify_state_id_address_result), + eql(state_id_address_resolution_result), ) expect(result.ipp_enrollment_in_progress).to eql(true) expect(proof.same_address_as_id).to eq(applicant_pii[:same_address_as_id]) @@ -270,7 +241,7 @@ let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS } it 'calls ThreatMetrixPlugin' do - expect(threatmetrix_plugin).to receive(:call).with( + expect(progressive_proofer.threatmetrix_plugin).to receive(:call).with( applicant_pii:, current_sp:, request_ip:, @@ -281,8 +252,8 @@ proof end - it 'calls InstantVerifyResidentialAddressPlugin' do - expect(instant_verify_residential_address_plugin).to receive(:call).with( + it 'calls ResidentialAddressPlugin' do + expect(progressive_proofer.residential_address_plugin).to receive(:call).with( applicant_pii:, current_sp:, ipp_enrollment_in_progress: true, @@ -291,11 +262,11 @@ proof end - it 'calls InstantVerifyStateIdAddressPlugin' do - expect(instant_verify_state_id_address_plugin).to receive(:call).with( + it 'calls StateIdAddressPlugin' do + expect(progressive_proofer.state_id_address_plugin).to receive(:call).with( applicant_pii:, current_sp:, - instant_verify_residential_address_result:, + residential_address_resolution_result:, ipp_enrollment_in_progress: true, timer: an_instance_of(JobHelpers::Timer), ).and_call_original @@ -303,10 +274,10 @@ end it 'calls AamvaPlugin' do - expect(aamva_plugin).to receive(:call).with( + expect(progressive_proofer.aamva_plugin).to receive(:call).with( applicant_pii:, current_sp:, - instant_verify_state_id_address_result:, + state_id_address_resolution_result:, ipp_enrollment_in_progress: true, timer: an_instance_of(JobHelpers::Timer), ).and_call_original @@ -316,11 +287,11 @@ it 'returns a ResultAdjudicator' do proof.tap do |result| expect(result).to be_an_instance_of(Proofing::Resolution::ResultAdjudicator) - expect(result.resolution_result).to eql(instant_verify_state_id_address_result) + expect(result.resolution_result).to eql(state_id_address_resolution_result) expect(result.state_id_result).to eql(aamva_result) expect(result.device_profiling_result).to eql(threatmetrix_result) expect(result.residential_resolution_result).to( - eql(instant_verify_residential_address_result), + eql(residential_address_resolution_result), ) expect(result.ipp_enrollment_in_progress).to eql(true) expect(result.same_address_as_id).to eql('false') @@ -339,12 +310,15 @@ it 'does not pass the phone number to plugins' do expected_applicant_pii = applicant_pii.except(:best_effort_phone_number_for_socure) - [ - aamva_plugin, - instant_verify_residential_address_plugin, - instant_verify_state_id_address_plugin, - threatmetrix_plugin, - ].each do |plugin| + plugin_methods = %i[ + aamva_plugin + residential_address_plugin + state_id_address_plugin + threatmetrix_plugin + ] + + plugin_methods.each do |plugin_method_name| + plugin = progressive_proofer.send(plugin_method_name) expect(plugin).to receive(:call).with( hash_including( applicant_pii: expected_applicant_pii, @@ -356,4 +330,166 @@ end end end + + describe '#proofing_vendor' do + let(:idv_resolution_default_vendor) { :default_vendor } + let(:idv_resolution_alternate_vendor) { :alternate_vendor } + let(:idv_resolution_alternate_vendor_percent) { 0 } + + subject(:proofing_vendor) { progressive_proofer.proofing_vendor } + + before do + allow(IdentityConfig.store).to receive(:idv_resolution_default_vendor). + and_return(idv_resolution_default_vendor) + allow(IdentityConfig.store).to receive(:idv_resolution_alternate_vendor). + and_return(idv_resolution_alternate_vendor) + allow(IdentityConfig.store).to receive(:idv_resolution_alternate_vendor_percent). + and_return(idv_resolution_alternate_vendor_percent) + end + + context 'when default is set to 100%' do + it 'uses the default' do + expect(proofing_vendor).to eql(:default_vendor) + end + end + + context 'when alternate is set to 100%' do + let(:idv_resolution_alternate_vendor_percent) { 100 } + + it 'uses the alternate' do + expect(proofing_vendor).to eql(:alternate_vendor) + end + end + + context 'when no alternate is set' do + let(:idv_resolution_alternate_vendor) { :none } + + it 'uses default' do + expect(proofing_vendor).to eql(:default_vendor) + end + + context 'and alternate is set to > 0' do + let(:idv_resolution_alternate_vendor_percent) { 100 } + it 'uses default' do + expect(proofing_vendor).to eql(:default_vendor) + end + end + end + end + + describe '#residential_address_plugin' do + let(:proofing_vendor) { nil } + + before do + allow(progressive_proofer).to receive(:proofing_vendor).and_return(proofing_vendor) + end + + context 'when proofing_vendor is :instant_verify' do + let(:proofing_vendor) { :instant_verify } + + it 'returns ResidentialAddressPlugin with an InstantVerify proofer' do + expect(progressive_proofer.residential_address_plugin).to be_an_instance_of( + Proofing::Resolution::Plugins::ResidentialAddressPlugin, + ) + + expect(progressive_proofer.residential_address_plugin.proofer).to be_an_instance_of( + Proofing::LexisNexis::InstantVerify::Proofer, + ) + end + end + + context 'when proofing_vendor is :mock' do + let(:proofing_vendor) { :mock } + + it 'returns ResidentialAddressPlugin with a mock proofer' do + expect(progressive_proofer.residential_address_plugin).to be_an_instance_of( + Proofing::Resolution::Plugins::ResidentialAddressPlugin, + ) + + expect(progressive_proofer.residential_address_plugin.proofer).to be_an_instance_of( + Proofing::Mock::ResolutionMockClient, + ) + end + end + + context 'when proofing_vendor is :socure_kyc' do + let(:proofing_vendor) { :socure_kyc } + + it 'returns ResidentialAddressPlugin with a Socure proofer' do + expect(progressive_proofer.residential_address_plugin).to be_an_instance_of( + Proofing::Resolution::Plugins::ResidentialAddressPlugin, + ) + + expect(progressive_proofer.residential_address_plugin.proofer).to be_an_instance_of( + Proofing::Socure::IdPlus::Proofer, + ) + end + end + + context 'when proofing_vendor is another value' do + let(:proofing_vendor) { :a_dog } + + it 'raises an error' do + expect { progressive_proofer.residential_address_plugin }.to raise_error + end + end + end + + describe '#state_id_address_plugin' do + let(:proofing_vendor) { nil } + + before do + allow(progressive_proofer).to receive(:proofing_vendor).and_return(proofing_vendor) + end + + context 'when proofing_vendor is :instant_verify' do + let(:proofing_vendor) { :instant_verify } + + it 'returns StateIdAddressPlugin with an InstantVerify proofer' do + expect(progressive_proofer.state_id_address_plugin).to be_an_instance_of( + Proofing::Resolution::Plugins::StateIdAddressPlugin, + ) + + expect(progressive_proofer.state_id_address_plugin.proofer).to be_an_instance_of( + Proofing::LexisNexis::InstantVerify::Proofer, + ) + end + end + + context 'when proofing_vendor is :socure_kyc' do + let(:proofing_vendor) { :socure_kyc } + + it 'returns StateIdAddressPlugin with a Socure proofer' do + expect(progressive_proofer.state_id_address_plugin).to be_an_instance_of( + Proofing::Resolution::Plugins::StateIdAddressPlugin, + ) + + expect(progressive_proofer.state_id_address_plugin.proofer).to be_an_instance_of( + Proofing::Socure::IdPlus::Proofer, + ) + end + end + + context 'when proofing_vendor is :mock' do + let(:proofing_vendor) { :mock } + + it 'returns StateIdAddressPlugin with a mock proofer' do + expect(progressive_proofer.state_id_address_plugin).to be_an_instance_of( + Proofing::Resolution::Plugins::StateIdAddressPlugin, + ) + + expect(progressive_proofer.state_id_address_plugin.proofer).to be_an_instance_of( + Proofing::Mock::ResolutionMockClient, + ) + end + end + + context 'when proofing_vendor is another value' do + let(:proofing_vendor) { :🦨 } + + it 'raises an error' do + expect { progressive_proofer.state_id_address_plugin }.to raise_error + end + end + end end diff --git a/spec/services/proofing/socure/id_plus/proofer_spec.rb b/spec/services/proofing/socure/id_plus/proofer_spec.rb index 9393dd588c2..a95c06893e9 100644 --- a/spec/services/proofing/socure/id_plus/proofer_spec.rb +++ b/spec/services/proofing/socure/id_plus/proofer_spec.rb @@ -172,6 +172,17 @@ end end + context 'when applicant includes extra fields' do + let(:applicant) do + { + some_weird_field_the_proofer_is_not_expecting: ':ohno:', + } + end + it 'does not raise an error' do + expect { result }.not_to raise_error + end + end + context 'when request times out' do before do stub_request(:post, URI.join(base_url, '/api/3.0/EmailAuthScore').to_s). @@ -224,7 +235,7 @@ end it 'includes exception details' do - expect(result.exception).to be_an_instance_of(Proofing::Socure::IdPlus::RequestError) + expect(result.exception).to be_an_instance_of(Proofing::Socure::IdPlus::Request::Error) end end end @@ -253,7 +264,7 @@ end it 'includes exception details' do - expect(result.exception).to be_an_instance_of(Proofing::Socure::IdPlus::RequestError) + expect(result.exception).to be_an_instance_of(Proofing::Socure::IdPlus::Request::Error) end end end @@ -278,7 +289,7 @@ end it 'includes exception details' do - expect(result.exception).to be_an_instance_of(Proofing::Socure::IdPlus::RequestError) + expect(result.exception).to be_an_instance_of(Proofing::Socure::IdPlus::Request::Error) end end end diff --git a/spec/services/proofing/socure/id_plus/request_spec.rb b/spec/services/proofing/socure/id_plus/request_spec.rb index 7ff73f5d5e8..333ee26285d 100644 --- a/spec/services/proofing/socure/id_plus/request_spec.rb +++ b/spec/services/proofing/socure/id_plus/request_spec.rb @@ -148,20 +148,20 @@ ) end - it 'raises RequestError' do + it 'raises Request::Error' do expect do request.send_request end.to raise_error( - Proofing::Socure::IdPlus::RequestError, + Proofing::Socure::IdPlus::Request::Error, 'Request-specific error message goes here (400)', ) end - it 'includes reference_id on RequestError' do + it 'includes reference_id on Request::Error' do expect do request.send_request end.to raise_error( - Proofing::Socure::IdPlus::RequestError, + Proofing::Socure::IdPlus::Request::Error, ) do |err| expect(err.reference_id).to eql('a-big-unique-reference-id') end @@ -186,11 +186,11 @@ ) end - it 'raises RequestError' do + it 'raises Request::Error' do expect do request.send_request end.to raise_error( - Proofing::Socure::IdPlus::RequestError, + Proofing::Socure::IdPlus::Request::Error, 'Request-specific error message goes here (401)', ) end @@ -205,10 +205,10 @@ ) end - it 'raises RequestError' do + it 'raises Request::Error' do expect do request.send_request - end.to raise_error(Proofing::Socure::IdPlus::RequestError) + end.to raise_error(Proofing::Socure::IdPlus::Request::Error) end end @@ -229,8 +229,8 @@ to_raise(Errno::ECONNRESET) end - it 'raises a RequestError' do - expect { request.send_request }.to raise_error Proofing::Socure::IdPlus::RequestError + it 'raises a Request::Error' do + expect { request.send_request }.to raise_error Proofing::Socure::IdPlus::Request::Error end end end From 94e98d7a6077540c18052d1f57d9de55a3885ee4 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Mon, 25 Nov 2024 07:38:30 -0500 Subject: [PATCH 13/23] Document usage of Lookbook for ViewComponents (#11540) changelog: Internal, Documentation, Document usage of Lookbook for ViewComponents --- docs/frontend.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/frontend.md b/docs/frontend.md index 50ea1687641..3b3a48c0d96 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -175,6 +175,14 @@ and independent view components, rendered server-side. For more information, refer to the [components `README.md`](../app/components/README.md). +To preview components and their available options, we use [Lookbook](https://lookbook.build/) to +generate a navigable index of our available components. These previews are available at the [`/components/` route](http://localhost:3000/components/) +in local development, review applications, and in the `dev` environment. When adding a new component +or an option to an existing component, you should also make this component or option available in +Lookbook previews, found under [`spec/components/previews`](https://github.com/18F/identity-idp/tree/main/spec/components/previews). +Refer to [Lookbook's _Previews Overview_ documentation](https://lookbook.build/guide/previews) for +more information on how to author Lookbook previews. + #### React For non-trivial client-side interactivity, we use [React](https://reactjs.org/) to build and combine From f53a5fdfaa1b98f6afb2c2fc4727ca12f1629c3b Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Mon, 25 Nov 2024 07:39:42 -0500 Subject: [PATCH 14/23] Upgrade DAP to v8.4 release (#11539) * Upgrade DAP to v8.4 release changelog: Internal, Analytics, Upgrade Digital Analytics Program to v8.4 release * Try fixing patch --- app/javascript/packages/analytics/Makefile | 2 +- .../packages/analytics/digital-analytics-program.patch | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/app/javascript/packages/analytics/Makefile b/app/javascript/packages/analytics/Makefile index 211d2567d23..9edd73fe61a 100644 --- a/app/javascript/packages/analytics/Makefile +++ b/app/javascript/packages/analytics/Makefile @@ -1,4 +1,4 @@ -DAP_SHA ?= 7c14bb3 +DAP_SHA ?= eebc4d1 digital-analytics-program.js: digital-analytics-program-$(DAP_SHA).js digital-analytics-program.patch patch -p1 $^ --output $@ diff --git a/app/javascript/packages/analytics/digital-analytics-program.patch b/app/javascript/packages/analytics/digital-analytics-program.patch index a2652e478f5..3700721caba 100644 --- a/app/javascript/packages/analytics/digital-analytics-program.patch +++ b/app/javascript/packages/analytics/digital-analytics-program.patch @@ -1,8 +1,2 @@ -73a74 -> GA4Object.defer = true; -785d785 -< var piiRegex = []; -900c900 -< piiRegex.forEach(function (pii) { ---- -> window.piiRegex.forEach(function (pii) { +87a88 +> GA4Object.defer = true; From 0ad50f95b31b8e4cb1c721e1488f7588061f2351 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Mon, 25 Nov 2024 07:59:20 -0500 Subject: [PATCH 15/23] Add uncapped preload headers for style, script assets (#11504) * Add unlimited preload headers for style, script assets changelog: Internal, Performance, Add preload headers for all style, script assets * Update spec assertions for dropped whitespace * Add specs * Append with string mutation See: https://github.com/18F/identity-idp/pull/11504/files#r1841273216 Co-authored-by: Zach Margolis * Use plain tag helper for generating script tag We don't need any of this anymore: - Asset pipeline lookup - Preload headers handling - Server push - Multiple source concatenation - Nonce handling crossorigin as a boolean attribute is same as crossorigin=anonymous * Add spec for preload_links_header attribute handling * Evaluate crossorigin once Previously evaluated per script, even though the result would always be the same * Micro-optimize append * Lowercase header key * Use presence to toggle between true/nil * Sync specs to use lowercase link key --------- Co-authored-by: Zach Margolis --- app/helpers/script_helper.rb | 22 ++++++-- app/helpers/stylesheet_helper.rb | 8 ++- app/services/asset_preload_linker.rb | 12 +++++ spec/helpers/script_helper_spec.rb | 16 +++++- spec/helpers/stylesheet_helper_spec.rb | 2 +- spec/services/asset_preload_linker_spec.rb | 63 ++++++++++++++++++++++ 6 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 app/services/asset_preload_linker.rb create mode 100644 spec/services/asset_preload_linker_spec.rb diff --git a/app/helpers/script_helper.rb b/app/helpers/script_helper.rb index 104e3cec3fe..ae35373dc69 100644 --- a/app/helpers/script_helper.rb +++ b/app/helpers/script_helper.rb @@ -14,14 +14,26 @@ def render_javascript_pack_once_tags(...) javascript_packs_tag_once(...) return if @scripts.blank? concat javascript_assets_tag + crossorigin = local_crossorigin_sources?.presence @scripts.each do |name, (url_params, attributes)| asset_sources.get_sources(name).each do |source| - concat javascript_include_tag( - UriService.add_params(source, url_params), + integrity = asset_sources.get_integrity(source) + + if attributes[:preload_links_header] != false + AssetPreloadLinker.append( + headers: response.headers, + as: :script, + url: source, + crossorigin:, + integrity:, + ) + end + + concat tag.script( + src: UriService.add_params(source, url_params), **attributes, - crossorigin: local_crossorigin_sources? ? true : nil, - integrity: asset_sources.get_integrity(source), - nopush: false, + crossorigin:, + integrity:, ) end end diff --git a/app/helpers/stylesheet_helper.rb b/app/helpers/stylesheet_helper.rb index 2becc651754..c607bdac62c 100644 --- a/app/helpers/stylesheet_helper.rb +++ b/app/helpers/stylesheet_helper.rb @@ -13,7 +13,13 @@ def stylesheet_tag_once(*names) def render_stylesheet_once_tags(*names) stylesheet_tag_once(*names) if names.present? return if @stylesheets.blank? - safe_join(@stylesheets.map { |stylesheet| stylesheet_link_tag(stylesheet, nopush: false) }) + safe_join( + @stylesheets.map do |stylesheet| + url = stylesheet_path(stylesheet) + AssetPreloadLinker.append(headers: response.headers, as: :style, url:) + tag.link(rel: :stylesheet, href: url) + end, + ) end end # rubocop:enable Rails/HelperInstanceVariable diff --git a/app/services/asset_preload_linker.rb b/app/services/asset_preload_linker.rb new file mode 100644 index 00000000000..13885272cc7 --- /dev/null +++ b/app/services/asset_preload_linker.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AssetPreloadLinker + def self.append(headers:, as:, url:, crossorigin: false, integrity: nil) + header = +headers['link'].to_s + header << ',' if header != '' + header << "<#{url}>;rel=preload;as=#{as}" + header << ';crossorigin' if crossorigin + header << ";integrity=#{integrity}" if integrity + headers['link'] = header + end +end diff --git a/spec/helpers/script_helper_spec.rb b/spec/helpers/script_helper_spec.rb index a7b256ba506..a13e12aa403 100644 --- a/spec/helpers/script_helper_spec.rb +++ b/spec/helpers/script_helper_spec.rb @@ -82,8 +82,8 @@ render_javascript_pack_once_tags expect(response.headers['link']).to eq( - '; rel=preload; as=script,' \ - '; rel=preload; as=script', + ';rel=preload;as=script,' \ + ';rel=preload;as=script', ) expect(response.headers['link']).to_not include('nopush') end @@ -107,6 +107,18 @@ end end + context 'with preload links header disabled' do + before do + javascript_packs_tag_once('application', preload_links_header: false) + end + + it 'does not append preload header' do + render_javascript_pack_once_tags + + expect(response.headers['link']).to eq(';rel=preload;as=script') + end + end + context 'with attributes' do before do javascript_packs_tag_once('track-errors', defer: true) diff --git a/spec/helpers/stylesheet_helper_spec.rb b/spec/helpers/stylesheet_helper_spec.rb index 9c4a9835bae..8237c2690ea 100644 --- a/spec/helpers/stylesheet_helper_spec.rb +++ b/spec/helpers/stylesheet_helper_spec.rb @@ -34,7 +34,7 @@ it 'adds preload header without nopush attribute' do render_stylesheet_once_tags - expect(response.headers['link']).to eq('; rel=preload; as=style') + expect(response.headers['link']).to eq(';rel=preload;as=style') expect(response.headers['link']).to_not include('nopush') end end diff --git a/spec/services/asset_preload_linker_spec.rb b/spec/services/asset_preload_linker_spec.rb new file mode 100644 index 00000000000..be302316c2c --- /dev/null +++ b/spec/services/asset_preload_linker_spec.rb @@ -0,0 +1,63 @@ +require 'rails_helper' + +RSpec.describe AssetPreloadLinker do + describe '.append' do + let(:link) { nil } + let(:as) { 'script' } + let(:url) { '/script.js' } + let(:crossorigin) { nil } + let(:integrity) { nil } + let(:headers) { { 'link' => link } } + subject(:result) do + AssetPreloadLinker.append(**{ headers:, as:, url:, crossorigin:, integrity: }.compact) + end + + context 'with absent link value' do + let(:link) { nil } + + it 'returns a string with only the appended link' do + expect(result).to eq(';rel=preload;as=script') + end + end + + context 'with empty link value' do + let(:link) { '' } + + it 'returns a string with only the appended link' do + expect(result).to eq(';rel=preload;as=script') + end + end + + context 'with non-empty link value' do + let(:link) { ';rel=preload;as=script' } + + it 'returns a comma-separated link value of the new and existing link' do + expect(result).to eq(';rel=preload;as=script,;rel=preload;as=script') + end + + context 'with existing link value as frozen string' do + let(:link) { ';rel=preload;as=script'.freeze } + + it 'returns a comma-separated link value of the new and existing link' do + expect(result).to eq(';rel=preload;as=script,;rel=preload;as=script') + end + end + end + + context 'with crossorigin option' do + let(:crossorigin) { true } + + it 'includes crossorigin link param' do + expect(result).to eq(';rel=preload;as=script;crossorigin') + end + end + + context 'with integrity option' do + let(:integrity) { 'abc123' } + + it 'includes integrity link param' do + expect(result).to eq(';rel=preload;as=script;integrity=abc123') + end + end + end +end From 0aa1128a8c92a8c97d063140aa79b6b3e7df8506 Mon Sep 17 00:00:00 2001 From: Mitchell Henke Date: Mon, 25 Nov 2024 09:10:38 -0600 Subject: [PATCH 16/23] Update Data Request script to compute requesting issuer and have configurable depth (#11541) * Update Data Request script to compute requesting issuer and have configurable depth changelog: Internal, Scripts, Update DataRequest script to compute requesting issuer and have configurable depth * add log for multiple service providers * Update lib/data_pull.rb Co-authored-by: Zach Margolis * Update lib/data_pull.rb Co-authored-by: Zach Margolis --------- Co-authored-by: Zach Margolis --- lib/data_pull.rb | 28 ++++++++++++++++++++++++++-- lib/script_base.rb | 7 +++++++ spec/lib/data_pull_spec.rb | 20 ++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/lib/data_pull.rb b/lib/data_pull.rb index f2c1be32320..35603fe46a1 100644 --- a/lib/data_pull.rb +++ b/lib/data_pull.rb @@ -162,14 +162,22 @@ def run(args:, config:) ActiveRecord::Base.connection.execute('SET statement_timeout = 0') uuids = args + requesting_issuers = + config.requesting_issuers.presence || compute_requesting_issuers(uuids) + users, missing_uuids = uuids.map do |uuid| DataRequests::Deployed::LookupUserByUuid.new(uuid).call || uuid end.partition { |u| u.is_a?(User) } - shared_device_users = DataRequests::Deployed::LookupSharedDeviceUsers.new(users).call + shared_device_users = + if config.depth && config.depth > 0 + DataRequests::Deployed::LookupSharedDeviceUsers.new(users, config.depth).call + else + users + end output = shared_device_users.map do |user| - DataRequests::Deployed::CreateUserReport.new(user, config.requesting_issuers).call + DataRequests::Deployed::CreateUserReport.new(user, requesting_issuers).call end if config.include_missing? @@ -198,6 +206,22 @@ def run(args:, config:) json: output, ) end + + private + + def compute_requesting_issuers(uuids) + service_providers = ServiceProviderIdentity.where(uuid: uuids).pluck(:service_provider) + return nil if service_providers.empty? + service_provider, _count = service_providers.tally.max_by { |_sp, count| count } + + if service_providers.count > 1 + warn "Multiple computed service providers: #{service_providers.join(', ')}" + end + + warn "Computed service provider #{service_provider}" + + Array(service_provider) + end end class ProfileSummary diff --git a/lib/script_base.rb b/lib/script_base.rb index 01352c25dee..c6e0c95f790 100644 --- a/lib/script_base.rb +++ b/lib/script_base.rb @@ -33,6 +33,7 @@ def reason_arg? :show_help, :requesting_issuers, :deflate, + :depth, :reason, keyword_init: true, ) do @@ -111,6 +112,12 @@ def option_parser config.requesting_issuers << issuer end + opts.on('--depth=DEPTH', <<-MSG) do |depth| + depth of connected devices (used for ig-request task) + MSG + config.depth = depth.to_i + end + opts.on('--help') do config.show_help = true end diff --git a/spec/lib/data_pull_spec.rb b/spec/lib/data_pull_spec.rb index 640f0741544..d772f9abfaf 100644 --- a/spec/lib/data_pull_spec.rb +++ b/spec/lib/data_pull_spec.rb @@ -319,6 +319,26 @@ expect(result.subtask).to eq('ig-request') expect(result.uuids).to eq([user.uuid]) end + + context 'with SP UUID argument and no requesting issuer' do + let(:args) { [identity.uuid] } + let(:config) { ScriptBase::Config.new } + + it 'runs the report with computed requesting issuer', aggregate_failures: true do + expect(result.table).to be_nil + expect(result.json.first.keys).to contain_exactly( + :user_id, + :login_uuid, + :requesting_issuer_uuid, + :email_addresses, + :mfa_configurations, + :user_events, + ) + + expect(result.subtask).to eq('ig-request') + expect(result.uuids).to eq([user.uuid]) + end + end end end From d1f9c01aa830e52172cff3b47f4c09188b08d528 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:39:19 -0500 Subject: [PATCH 17/23] Log Face/Touch setup A/B test from session value (#11549) changelog: Internal, A/B Tests, Fix logging for A/B test to recommend platform authenticator to SMS users --- ...ebauthn_platform_recommended_controller.rb | 10 +++++++++ .../users/webauthn_setup_controller.rb | 1 + app/services/analytics_events.rb | 4 ++++ config/initializers/ab_tests.rb | 2 +- ...hn_platform_recommended_controller_spec.rb | 21 +++++++++++++++++++ .../users/webauthn_setup_controller_spec.rb | 15 +++++++++++++ 6 files changed, 52 insertions(+), 1 deletion(-) diff --git a/app/controllers/users/webauthn_platform_recommended_controller.rb b/app/controllers/users/webauthn_platform_recommended_controller.rb index 039d8acb56a..7c8395dfdcc 100644 --- a/app/controllers/users/webauthn_platform_recommended_controller.rb +++ b/app/controllers/users/webauthn_platform_recommended_controller.rb @@ -15,12 +15,22 @@ def new def create analytics.webauthn_platform_recommended_submitted(opted_to_add: opted_to_add?) + store_webauthn_platform_recommended_in_session if opted_to_add? current_user.update(webauthn_platform_recommended_dismissed_at: Time.zone.now) redirect_to dismiss_redirect_path end private + def store_webauthn_platform_recommended_in_session + user_session[:webauthn_platform_recommended] = + if in_account_creation_flow? + :account_creation + else + :authentication + end + end + def opted_to_add? params[:add_method].present? end diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index 66a4694a668..b67f5b04275 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -161,6 +161,7 @@ def process_valid_webauthn(form) def analytics_properties { in_account_creation_flow: user_session[:in_account_creation_flow] || false, + webauthn_platform_recommended: user_session[:webauthn_platform_recommended], attempts: mfa_attempts_count, } end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 9efc2ca3f4d..debfcd4d621 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -5361,6 +5361,8 @@ def multi_factor_auth_phone_setup( # @param [String, nil] aaguid AAGUID value of WebAuthn device # @param [String[], nil] unknown_transports Array of unrecognized WebAuthn transports, intended to # be used in case of future specification changes. + # @param [:authentication, :account_creation, nil] webauthn_platform_recommended A/B test for + # recommended Face or Touch Unlock setup, if applicable. def multi_factor_auth_setup( success:, multi_factor_auth_method:, @@ -5384,6 +5386,7 @@ def multi_factor_auth_setup( attempts: nil, aaguid: nil, unknown_transports: nil, + webauthn_platform_recommended: nil, **extra ) track_event( @@ -5410,6 +5413,7 @@ def multi_factor_auth_setup( attempts:, aaguid:, unknown_transports:, + webauthn_platform_recommended:, **extra, ) end diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index cb2931bac37..ea7288631d0 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -87,7 +87,7 @@ def self.all should_log: [ :webauthn_platform_recommended_visited, :webauthn_platform_recommended_submitted, - :webauthn_setup_submitted, + 'Multi-Factor Authentication Setup', ].to_set, buckets: { recommend_for_account_creation: diff --git a/spec/controllers/users/webauthn_platform_recommended_controller_spec.rb b/spec/controllers/users/webauthn_platform_recommended_controller_spec.rb index 1e9e334500d..853b2b9fe18 100644 --- a/spec/controllers/users/webauthn_platform_recommended_controller_spec.rb +++ b/spec/controllers/users/webauthn_platform_recommended_controller_spec.rb @@ -58,6 +58,11 @@ end end + it 'does not assign recommended session value' do + expect { response }.not_to change { controller.user_session[:webauthn_platform_recommended] }. + from(nil) + end + it 'redirects user to after sign in path' do expect(controller).to receive(:after_sign_in_path_for).with(user).and_return(account_path) @@ -92,6 +97,22 @@ it 'redirects user to set up platform authenticator' do expect(response).to redirect_to(webauthn_setup_path(platform: true)) end + + it 'assigns recommended session value to recommendation flow' do + expect { response }.to change { controller.user_session[:webauthn_platform_recommended] }. + from(nil).to(:authentication) + end + + context 'user is creating account' do + before do + allow(controller).to receive(:in_account_creation_flow?).and_return(true) + end + + it 'assigns recommended session value to recommendation flow' do + expect { response }.to change { controller.user_session[:webauthn_platform_recommended] }. + from(nil).to(:account_creation) + end + end end end end diff --git a/spec/controllers/users/webauthn_setup_controller_spec.rb b/spec/controllers/users/webauthn_setup_controller_spec.rb index d6416d45ae4..8e0e47be037 100644 --- a/spec/controllers/users/webauthn_setup_controller_spec.rb +++ b/spec/controllers/users/webauthn_setup_controller_spec.rb @@ -138,6 +138,21 @@ success: true, ) end + + context 'with setup from sms recommendation' do + before do + controller.user_session[:webauthn_platform_recommended] = :authentication + end + + it 'logs setup event with session value' do + patch :confirm, params: params + + expect(@analytics).to have_logged_event( + 'Multi-Factor Authentication Setup', + hash_including(webauthn_platform_recommended: :authentication), + ) + end + end end end From 143ed966fcf4fc8442ed86cca64224a869ed5d4c Mon Sep 17 00:00:00 2001 From: Mitchell Henke Date: Mon, 25 Nov 2024 12:49:07 -0600 Subject: [PATCH 18/23] Do not return User UUID in requesting_issuer_uuid when generating user report (#11553) changelog: Internal, Reporting, Do not return User UUID in requesting_issuer_uuid when generating user report --- lib/data_requests/deployed/create_user_report.rb | 1 - spec/lib/data_requests/deployed/create_user_report_spec.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/data_requests/deployed/create_user_report.rb b/lib/data_requests/deployed/create_user_report.rb index 062e65ab571..c2980d0b489 100644 --- a/lib/data_requests/deployed/create_user_report.rb +++ b/lib/data_requests/deployed/create_user_report.rb @@ -32,7 +32,6 @@ def mfa_configurations_report end def requesting_issuer_uuid - return user.uuid if requesting_issuers.blank? user.agency_identities.where(agency: requesting_agencies).first&.uuid || "NonSPUser##{user.id}" end diff --git a/spec/lib/data_requests/deployed/create_user_report_spec.rb b/spec/lib/data_requests/deployed/create_user_report_spec.rb index 8b66c950bea..5658d2459b6 100644 --- a/spec/lib/data_requests/deployed/create_user_report_spec.rb +++ b/spec/lib/data_requests/deployed/create_user_report_spec.rb @@ -9,7 +9,7 @@ expect(result[:user_id]).to eq(user.id) expect(result[:login_uuid]).to eq(user.uuid) - expect(result[:requesting_issuer_uuid]).to eq(user.uuid) + expect(result[:requesting_issuer_uuid]).to eq("NonSPUser##{user.id}") expect(result[:email_addresses]).to be_a(Array) expect(result[:mfa_configurations]).to be_a(Hash) expect(result[:user_events]).to be_a(Array) From c45cac10669f189a5910b12d1db075aa4cfd7294 Mon Sep 17 00:00:00 2001 From: Jenny Verdeyen Date: Mon, 25 Nov 2024 14:38:56 -0500 Subject: [PATCH 19/23] LG-15171 Fixes redirect for PUT (#11545) changelog: Bug Fixes, In-person proofing, fixes redirect for put for state id routes renaming --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 0d832c5e8bf..a01b1257562 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -412,7 +412,7 @@ # Deprecated route - temporary redirect while state id changes are rolled out get '/in_person_proofing/state_id' => redirect('verify/in_person/state_id', status: 307) - put '/in_person_proofing/state_id' => 'in_person/state_id#update' + put '/in_person_proofing/state_id' => redirect('verify/in_person/state_id', status: 307) get '/in_person' => 'in_person#index' get '/in_person/ready_to_verify' => 'in_person/ready_to_verify#show', From 5693220f0fb0b023e1b21e678d82ae211f132ded Mon Sep 17 00:00:00 2001 From: "Davida (she/they)" Date: Mon, 25 Nov 2024 16:38:48 -0500 Subject: [PATCH 20/23] Add analytics tracking for signature algorithm errors (#11552) * changelog: Internal, Analytics, Add tracking for sha256 change --- Gemfile | 2 +- Gemfile.lock | 6 +- app/controllers/saml_idp_controller.rb | 22 +++++ app/services/analytics_events.rb | 8 ++ spec/controllers/saml_idp_controller_spec.rb | 87 ++++++++++++++++++++ 5 files changed, 121 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 17262773376..8c3c853be75 100644 --- a/Gemfile +++ b/Gemfile @@ -74,7 +74,7 @@ gem 'rqrcode' gem 'ruby-progressbar' gem 'ruby-saml' gem 'safe_target_blank', '>= 1.0.2' -gem 'saml_idp', github: '18F/saml_idp', tag: '0.23.0-18f' +gem 'saml_idp', github: '18F/saml_idp', tag: '0.23.3-18f' gem 'scrypt' gem 'simple_form', '>= 5.0.2' gem 'stringex', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 9fe38995467..e27bf114bc0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -36,10 +36,10 @@ GIT GIT remote: https://github.com/18F/saml_idp.git - revision: 23b69117593e9b9217910af1dd627febd8d18cf4 - tag: 0.23.0-18f + revision: 752085a6f88cd3ce75ecc7a64afe064a0e4f9e35 + tag: 0.23.3-18f specs: - saml_idp (0.23.0.pre.18f) + saml_idp (0.23.3.pre.18f) activesupport builder faraday diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index a945f66259f..764f0e22fe8 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -136,6 +136,13 @@ def capture_analytics if result.success? && saml_request.signed? analytics_payload[:cert_error_details] = saml_request.cert_errors + + # analytics to determine if turning on SHA256 validation will break + # existing partners + if certs_different? + analytics_payload[:certs_different] = true + analytics_payload[:sha256_matching_cert] = sha256_alg_matching_cert_serial + end end analytics.saml_auth(**analytics_payload) @@ -147,6 +154,21 @@ def matching_cert_serial nil end + def sha256_alg_matching_cert + # if sha256_alg_matching_cert is nil, fallback to the "first" cert + saml_request.sha256_validation_matching_cert || + saml_request_service_provider&.ssl_certs&.first + rescue SamlIdp::XMLSecurity::SignedDocument::ValidationError + end + + def sha256_alg_matching_cert_serial + sha256_alg_matching_cert&.serial&.to_s + end + + def certs_different? + encryption_cert != sha256_alg_matching_cert + end + def log_external_saml_auth_request return unless external_saml_request? diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index debfcd4d621..2bcf7b14be0 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -6446,8 +6446,12 @@ def rules_of_use_visit # @param [Boolean] request_signed # @param [String] matching_cert_serial # matches the request certificate in a successful, signed request + # @param [Boolean] certs_different Whether the matching cert changes when SHA256 validations + # are turned on in the saml_idp gem # @param [Hash] cert_error_details Details for errors that occurred because of an invalid # signature + # @param [String] sha256_matching_cert serial of the cert that matches when sha256 validations + # are turned on # @param [String] unknown_authn_contexts space separated list of unknown contexts def saml_auth( success:, @@ -6465,6 +6469,8 @@ def saml_auth( matching_cert_serial:, error_details: nil, cert_error_details: nil, + certs_different: nil, + sha256_matching_cert: nil, unknown_authn_contexts: nil, **extra ) @@ -6485,6 +6491,8 @@ def saml_auth( request_signed:, matching_cert_serial:, cert_error_details:, + certs_different:, + sha256_matching_cert:, unknown_authn_contexts:, **extra, ) diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index 9777cfbc734..40b9f4fd4cf 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -1525,6 +1525,93 @@ def name_id_version(format_urn) ) end + context 'when request is using SHA1 as the signature method algorithm' do + let(:auth_settings) do + saml_settings( + overrides: { + security: { + authn_requests_signed:, + signature_method: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha1', + }, + }, + ) + end + + context 'when the certificate matches' do + it 'does not note that certs are different in the event' do + user.identities.last.update!(verified_attributes: ['email']) + generate_saml_response(user, auth_settings) + + expect(response.status).to eq(200) + expect(@analytics).to have_logged_event( + 'SAML Auth', hash_not_including( + certs_different: true, + sha256_matching_cert: matching_cert_serial, + ) + ) + end + end + + context 'when the certificate does not match' do + let(:wrong_cert) do + OpenSSL::X509::Certificate.new( + Rails.root.join('certs', 'sp', 'saml_test_sp2.crt').read, + ) + end + + before do + service_provider.update!(certs: [wrong_cert, saml_test_sp_cert]) + end + + it 'notes that certs are different in the event' do + user.identities.last.update!(verified_attributes: ['email']) + generate_saml_response(user, auth_settings) + + expect(response.status).to eq(200) + expect(@analytics).to have_logged_event( + 'SAML Auth', hash_including( + certs_different: true, + sha256_matching_cert: wrong_cert.serial.to_s, + ) + ) + end + end + end + + context 'when request is using SHA1 as the digest method algorithm' do + let(:auth_settings) do + saml_settings( + overrides: { + security: { + authn_requests_signed:, + digest_method: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha1', + }, + }, + ) + end + + it 'notes an error in the event' do + user.identities.last.update!(verified_attributes: ['email']) + generate_saml_response(user, auth_settings) + + expect(response.status).to eq(200) + expect(@analytics).to have_logged_event( + 'SAML Auth', hash_including( + request_signed: authn_requests_signed, + cert_error_details: [ + { + cert: '16692258094164984098', + error_code: :fingerprint_mismatch, + }, + { + cert: '14834808178619537243', error_code: :fingerprint_mismatch + }, + ], + ) + ) + end + end + context 'Certificate sig validation fails because of namespace bug' do let(:request_sp) { double } From 80e393feb3e236dc41a59e23471b477f4ed8f7ed Mon Sep 17 00:00:00 2001 From: "Luis H. Matos" Date: Mon, 25 Nov 2024 16:48:57 -0600 Subject: [PATCH 21/23] LG-15119 Update MKMR to split verified users by facial matching (#11557) * LG-15119 Update MKMR to split verified users by facial matching changelog: Internal, Reporting, Update MKMR to split verified useres by facial matching --- .../reporting/agency_and_sp_report.rb | 21 +++- .../reporting/total_user_count_report.rb | 75 ++++++++++--- .../reporting/agency_and_sp_report_spec.rb | 35 ++++-- .../reporting/total_user_count_report_spec.rb | 103 ++++++++++++++---- 4 files changed, 190 insertions(+), 44 deletions(-) diff --git a/app/services/reporting/agency_and_sp_report.rb b/app/services/reporting/agency_and_sp_report.rb index e120c81236d..2ab63c8320d 100644 --- a/app/services/reporting/agency_and_sp_report.rb +++ b/app/services/reporting/agency_and_sp_report.rb @@ -10,15 +10,26 @@ def initialize(report_date = Time.zone.today) def agency_and_sp_report idv_sps, auth_sps = service_providers.partition { |sp| sp.ial.present? && sp.ial >= 2 } + idv_agency_ids = idv_sps.map(&:agency_id).uniq idv_agencies, auth_agencies = active_agencies.partition do |agency| idv_agency_ids.include?(agency.id) end + idv_sps_facial_match, idv_sps_legacy = idv_sps.partition do |sp| + facial_match_issuers.include?(sp.issuer) + end + + idv_agency_facial_match_ids = idv_sps_facial_match.map(&:agency_id) + idv_facial_match_agencies, idv_legacy_agencies = idv_agencies.partition do |agency| + idv_agency_facial_match_ids.include?(agency.id) + end + [ ['', 'Number of apps (SPs)', 'Number of agencies and states'], ['Auth', auth_sps.count, auth_agencies.count], - ['IDV', idv_sps.count, idv_agencies.count], + ['IDV (Legacy IDV)', idv_sps_legacy.count, idv_legacy_agencies.count], + ['IDV (Facial matching)', idv_sps_facial_match.count, idv_facial_match_agencies.count], ['Total', auth_sps.count + idv_sps.count, auth_agencies.count + idv_agencies.count], ] rescue ActiveRecord::QueryCanceled => err @@ -54,5 +65,13 @@ def service_providers ServiceProvider.where(issuer: issuers).active.external end end + + def facial_match_issuers + @facial_match_issuers ||= Profile.where(active: true).where( + 'verified_at <= ?', + report_date.end_of_day, + ).where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS). + pluck(:initiating_service_provider_issuer).uniq + end end end diff --git a/app/services/reporting/total_user_count_report.rb b/app/services/reporting/total_user_count_report.rb index 7d372e282c1..62ac028906d 100644 --- a/app/services/reporting/total_user_count_report.rb +++ b/app/services/reporting/total_user_count_report.rb @@ -10,20 +10,43 @@ def initialize(report_date = Time.zone.today) def total_user_count_report [ - ['Metric', 'All Users', 'Verified users', 'Time Range Start', 'Time Range End'], - ['All-time count', total_user_count, verified_user_count, '-', report_date.to_date], - ['All-time fully registered', total_fully_registered, '-', '-', report_date.to_date], + [ + 'Metric', + 'All Users', + 'Verified users (Legacy IDV)', + 'Verified users (Facial Matching)', + 'Time Range Start', + 'Time Range End', + ], + [ + 'All-time count', + total_user_count, + verified_legacy_idv_user_count, + verified_facial_match_user_count, + '-', + report_date.to_date, + ], + [ + 'All-time fully registered', + total_fully_registered, + '-', + '-', + '-', + report_date.to_date, + ], [ 'New users count', new_user_count, - new_verified_user_count, + new_verified_legacy_idv_user_count, + new_verified_facial_match_user_count, current_month.begin.to_date, current_month.end.to_date, ], [ 'Annual users count', annual_total_user_count, - annual_verified_user_count, + annual_verified_legacy_idv_user_count, + annual_verified_facial_match_user_count, annual_start_date.to_date, annual_end_date.to_date, ], @@ -58,15 +81,35 @@ def total_user_count end end - def verified_user_count + def verified_legacy_idv_user_count Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where('verified_at <= ?', end_date).count + Profile.where(active: true).where( + 'verified_at <= ?', + end_date, + ).count - verified_facial_match_user_count end end - def new_verified_user_count + def verified_facial_match_user_count + @verified_facial_match_user_count ||= Reports::BaseReport.transaction_with_timeout do + Profile.where(active: true).where( + 'verified_at <= ?', + end_date, + ).where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS).count + end + end + + def new_verified_legacy_idv_user_count Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true).where(verified_at: current_month).count + Profile.where(active: true).where(verified_at: current_month).count - + new_verified_facial_match_user_count + end + end + + def new_verified_facial_match_user_count + @new_verified_facial_match_user_count ||= Reports::BaseReport.transaction_with_timeout do + Profile.where(active: true).where(verified_at: current_month). + where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS).count end end @@ -76,11 +119,17 @@ def annual_total_user_count end end - def annual_verified_user_count + def annual_verified_legacy_idv_user_count Reports::BaseReport.transaction_with_timeout do - Profile.where(active: true). - where(verified_at: annual_start_date..annual_end_date). - count + Profile.where(active: true).where(verified_at: annual_start_date..annual_end_date).count - + annual_verified_facial_match_user_count + end + end + + def annual_verified_facial_match_user_count + @annual_verified_facial_match_user_count ||= Reports::BaseReport.transaction_with_timeout do + Profile.where(active: true).where(verified_at: annual_start_date..annual_end_date). + where(idv_level: Profile::FACIAL_MATCH_IDV_LEVELS).count end end diff --git a/spec/services/reporting/agency_and_sp_report_spec.rb b/spec/services/reporting/agency_and_sp_report_spec.rb index 9711359f667..7ed81700074 100644 --- a/spec/services/reporting/agency_and_sp_report_spec.rb +++ b/spec/services/reporting/agency_and_sp_report_spec.rb @@ -53,7 +53,8 @@ [ header_row, ['Auth', 1, 1], - ['IDV', 0, 0], + ['IDV (Facial matching)', 0, 0], + ['IDV (Legacy IDV)', 0, 0], ['Total', 1, 1], ] end @@ -69,7 +70,8 @@ [ header_row, ['Auth', 0, 1], - ['IDV', 0, 0], + ['IDV (Facial matching)', 0, 0], + ['IDV (Legacy IDV)', 0, 0], ['Total', 0, 1], ] end @@ -94,7 +96,8 @@ [ header_row, ['Auth', 1, 1], - ['IDV', 0, 0], + ['IDV (Facial matching)', 0, 0], + ['IDV (Legacy IDV)', 0, 0], ['Total', 1, 1], ] end @@ -103,7 +106,8 @@ [ header_row, ['Auth', 1, 0], - ['IDV', 1, 1], + ['IDV (Facial matching)', 0, 0], + ['IDV (Legacy IDV)', 1, 1], ['Total', 2, 1], ] end @@ -127,7 +131,7 @@ end context 'when adding an IDV SP' do - let!(:idv_sp) do + let!(:idv_legacy_sp) do create( :service_provider, :external, @@ -138,15 +142,32 @@ ) end + let!(:idv_facial_match_sp) do + create( + :service_provider, + :external, + :idv, + :active, + agency:, + identities: [build(:service_provider_identity, service_provider: 'https://facialmatch.com')], + ) + end + let(:expected_report) do [ header_row, ['Auth', 0, 0], - ['IDV', 1, 1], - ['Total', 1, 1], + ['IDV (Facial matching)', 1, 1], + ['IDV (Legacy IDV)', 1, 0], + ['Total', 2, 1], ] end + before do + allow_any_instance_of(Reporting::AgencyAndSpReport).to receive(:facial_match_issuers). + and_return([idv_facial_match_sp.issuer]) + end + it 'counts the SP and its Agency as IDV' do expect(subject).to match_array(expected_report) end diff --git a/spec/services/reporting/total_user_count_report_spec.rb b/spec/services/reporting/total_user_count_report_spec.rb index 9135cd25e67..1f4ba98ef25 100644 --- a/spec/services/reporting/total_user_count_report_spec.rb +++ b/spec/services/reporting/total_user_count_report_spec.rb @@ -8,26 +8,43 @@ let(:expected_report) do [ - ['Metric', 'All Users', 'Verified users', 'Time Range Start', 'Time Range End'], - ['All-time count', expected_total_count, expected_verified_count, '-', Date.new(2021, 3, 1)], + [ + 'Metric', + 'All Users', + 'Verified users (Legacy IDV)', + 'Verified users (Facial Matching)', + 'Time Range Start', + 'Time Range End', + ], + [ + 'All-time count', + expected_total_count, + expected_verified_legacy_idv_count, + expected_verified_facial_match_count, + '-', + Date.new(2021, 3, 1), + ], [ 'All-time fully registered', expected_total_fully_registered, '-', '-', + '-', Date.new(2021, 3, 1), ], [ 'New users count', expected_new_count, - expected_new_verified_count, + expected_new_verified_legacy_idv_count, + expected_new_verified_facial_match_count, Date.new(2021, 3, 1), Date.new(2021, 3, 31), ], [ 'Annual users count', expected_annual_count, - expected_annual_verified_count, + expected_annual_verified_legacy_idv_count, + expected_annual_verified_facial_match_count, Date.new(2020, 10, 1), Date.new(2021, 9, 30), ], @@ -50,12 +67,15 @@ context 'with only a non-verified user' do before { create(:user) } let(:expected_total_count) { 1 } - let(:expected_verified_count) { 0 } + let(:expected_verified_legacy_idv_count) { 0 } + let(:expected_verified_facial_match_count) { 0 } let(:expected_total_fully_registered) { 0 } let(:expected_new_count) { 1 } - let(:expected_new_verified_count) { 0 } + let(:expected_new_verified_legacy_idv_count) { 0 } + let(:expected_new_verified_facial_match_count) { 0 } let(:expected_annual_count) { expected_total_count } - let(:expected_annual_verified_count) { 0 } + let(:expected_annual_verified_legacy_idv_count) { 0 } + let(:expected_annual_verified_facial_match_count) { 0 } it_behaves_like 'a report with the specified counts' end @@ -65,17 +85,20 @@ let!(:old_user) { create(:user, created_at: report_date - 13.months) } let(:expected_total_count) { 2 } - let(:expected_verified_count) { 0 } + let(:expected_verified_legacy_idv_count) { 0 } + let(:expected_verified_facial_match_count) { 0 } let(:expected_total_fully_registered) { 0 } let(:expected_new_count) { 1 } - let(:expected_new_verified_count) { 0 } + let(:expected_new_verified_legacy_idv_count) { 0 } + let(:expected_new_verified_facial_match_count) { 0 } let(:expected_annual_count) { 1 } - let(:expected_annual_verified_count) { 0 } + let(:expected_annual_verified_legacy_idv_count) { 0 } + let(:expected_annual_verified_facial_match_count) { 0 } it_behaves_like 'a report with the specified counts' end - context 'with one verified and one non-verified user' do + context 'with one legacy verified and one non-verified user' do before do user1 = create(:user) user2 = create(:user) @@ -86,12 +109,37 @@ user2.profiles.first.deactivate(:password_reset) end let(:expected_total_count) { 2 } - let(:expected_verified_count) { 1 } + let(:expected_verified_legacy_idv_count) { 1 } + let(:expected_verified_facial_match_count) { 0 } + let(:expected_total_fully_registered) { 0 } + let(:expected_new_count) { 2 } + let(:expected_new_verified_legacy_idv_count) { 1 } + let(:expected_new_verified_facial_match_count) { 0 } + let(:expected_annual_count) { expected_total_count } + let(:expected_annual_verified_legacy_idv_count) { 1 } + let(:expected_annual_verified_facial_match_count) { 0 } + + it_behaves_like 'a report with the specified counts' + end + + context 'with one facial match verified and one non-verified user' do + before do + user1 = create(:user) + user2 = create(:user) + create(:profile, :active, :facial_match_proof, user: user1) + create(:profile, :active, :verified, user: user2) + user2.profiles.first.deactivate(:password_reset) + end + let(:expected_total_count) { 2 } + let(:expected_verified_legacy_idv_count) { 0 } + let(:expected_verified_facial_match_count) { 1 } let(:expected_total_fully_registered) { 0 } let(:expected_new_count) { 2 } - let(:expected_new_verified_count) { 1 } + let(:expected_new_verified_legacy_idv_count) { 0 } + let(:expected_new_verified_facial_match_count) { 1 } let(:expected_annual_count) { expected_total_count } - let(:expected_annual_verified_count) { 1 } + let(:expected_annual_verified_legacy_idv_count) { 0 } + let(:expected_annual_verified_facial_match_count) { 1 } it_behaves_like 'a report with the specified counts' end @@ -104,12 +152,15 @@ # A suspended user is still a total user: let(:expected_total_count) { 1 } - let(:expected_verified_count) { 0 } + let(:expected_verified_legacy_idv_count) { 0 } + let(:expected_verified_facial_match_count) { 0 } let(:expected_total_fully_registered) { 0 } let(:expected_new_count) { 1 } - let(:expected_new_verified_count) { 0 } + let(:expected_new_verified_legacy_idv_count) { 0 } + let(:expected_new_verified_facial_match_count) { 0 } let(:expected_annual_count) { 1 } - let(:expected_annual_verified_count) { 0 } + let(:expected_annual_verified_legacy_idv_count) { 0 } + let(:expected_annual_verified_facial_match_count) { 0 } it_behaves_like 'a report with the specified counts' end @@ -119,12 +170,15 @@ # A user with a fraud rejection is still a total user let(:expected_total_count) { 1 } - let(:expected_verified_count) { 0 } + let(:expected_verified_legacy_idv_count) { 0 } + let(:expected_verified_facial_match_count) { 0 } let(:expected_total_fully_registered) { 1 } let(:expected_new_count) { 1 } - let(:expected_new_verified_count) { 0 } + let(:expected_new_verified_legacy_idv_count) { 0 } + let(:expected_new_verified_facial_match_count) { 0 } let(:expected_annual_count) { 1 } - let(:expected_annual_verified_count) { 0 } + let(:expected_annual_verified_legacy_idv_count) { 0 } + let(:expected_annual_verified_facial_match_count) { 0 } it_behaves_like 'a report with the specified counts' end @@ -137,12 +191,15 @@ end end let(:expected_total_count) { 3 } - let(:expected_verified_count) { 0 } + let(:expected_verified_legacy_idv_count) { 0 } + let(:expected_verified_facial_match_count) { 0 } let(:expected_total_fully_registered) { 2 } let(:expected_new_count) { 3 } - let(:expected_new_verified_count) { 0 } + let(:expected_new_verified_legacy_idv_count) { 0 } + let(:expected_new_verified_facial_match_count) { 0 } let(:expected_annual_count) { 3 } - let(:expected_annual_verified_count) { 0 } + let(:expected_annual_verified_legacy_idv_count) { 0 } + let(:expected_annual_verified_facial_match_count) { 0 } it_behaves_like 'a report with the specified counts' end From 3f0fd8c1d7f9b79f6c9d4602a9100b565ef6554a Mon Sep 17 00:00:00 2001 From: Doug Price Date: Mon, 25 Nov 2024 18:07:08 -0500 Subject: [PATCH 22/23] LG-15098: Run the Socure DocV result job in it's own queue. (#11548) Will have no effect unless `high_socure_docv` is added to the config `good_job_queues` [skip changelog] --- app/jobs/socure_docv_results_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/socure_docv_results_job.rb b/app/jobs/socure_docv_results_job.rb index 20221f23d68..ad3fa7bca04 100644 --- a/app/jobs/socure_docv_results_job.rb +++ b/app/jobs/socure_docv_results_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class SocureDocvResultsJob < ApplicationJob - queue_as :default + queue_as :high_socure_docv attr_reader :document_capture_session_uuid From fba9f9b5ae7e5817293dc9e953c2b3d2ace88fad Mon Sep 17 00:00:00 2001 From: Mitchell Henke Date: Tue, 26 Nov 2024 08:53:06 -0600 Subject: [PATCH 23/23] Log requesting signing and certificate serial in SAML Auth Request event (#11558) changelog: Internal, Logging, Log requesting signing and certificate serial in SAML Auth Request event --- app/controllers/saml_idp_controller.rb | 2 ++ app/services/analytics_events.rb | 6 ++++++ spec/controllers/saml_idp_controller_spec.rb | 13 +++++++++++++ spec/features/saml/saml_spec.rb | 6 ++++++ 4 files changed, 27 insertions(+) diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index 764f0e22fe8..55083feb324 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -180,6 +180,8 @@ def log_external_saml_auth_request force_authn: saml_request&.force_authn?, final_auth_request: sp_session[:final_auth_request], service_provider: saml_request&.issuer, + request_signed: saml_request.signed?, + matching_cert_serial:, unknown_authn_contexts:, user_fully_authenticated: user_fully_authenticated?, ) diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 2bcf7b14be0..2bc84a7d2cb 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -6505,6 +6505,8 @@ def saml_auth( # @param [Boolean] force_authn # @param [Boolean] final_auth_request # @param [String] service_provider + # @param [Boolean] request_signed + # @param [String] matching_cert_serial # @param [String] unknown_authn_contexts space separated list of unknown contexts # @param [Boolean] user_fully_authenticated # An external request for SAML Authentication was received @@ -6516,6 +6518,8 @@ def saml_auth_request( force_authn:, final_auth_request:, service_provider:, + request_signed:, + matching_cert_serial:, unknown_authn_contexts:, user_fully_authenticated:, **extra @@ -6529,6 +6533,8 @@ def saml_auth_request( force_authn:, final_auth_request:, service_provider:, + request_signed:, + matching_cert_serial:, unknown_authn_contexts:, user_fully_authenticated:, **extra, diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index 40b9f4fd4cf..c2f6cacea8d 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -779,6 +779,8 @@ def name_id_version(format_urn) requested_ial: Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF, service_provider: sp1_issuer, force_authn: false, + request_signed: true, + matching_cert_serial: saml_test_sp_cert_serial, user_fully_authenticated: true, } ) @@ -930,6 +932,8 @@ def name_id_version(format_urn) requested_ial: 'ialmax', service_provider: sp1_issuer, force_authn: false, + request_signed: true, + matching_cert_serial: saml_test_sp_cert_serial, user_fully_authenticated: true, } ) @@ -1221,6 +1225,8 @@ def name_id_version(format_urn) requested_ial: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, service_provider: 'http://localhost:3000', requested_aal_authn_context: Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + request_signed: true, + matching_cert_serial: saml_test_sp_cert_serial, force_authn: true, user_fully_authenticated: false, } @@ -2030,6 +2036,8 @@ def name_id_version(format_urn) requested_ial: Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, service_provider: 'http://localhost:3000', requested_aal_authn_context: Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + request_signed: true, + matching_cert_serial: saml_test_sp_cert_serial, force_authn: false, user_fully_authenticated: false, } @@ -2464,6 +2472,7 @@ def name_id_version(format_urn) service_provider: 'http://localhost:3000', requested_aal_authn_context: Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, force_authn: false, + request_signed: false, user_fully_authenticated: true, } ) @@ -2515,6 +2524,8 @@ def stub_requested_attributes service_provider: 'http://localhost:3000', requested_aal_authn_context: Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, force_authn: false, + request_signed: true, + matching_cert_serial: saml_test_sp_cert_serial, user_fully_authenticated: true, } ) @@ -2565,6 +2576,8 @@ def stub_requested_attributes service_provider: 'http://localhost:3000', requested_aal_authn_context: Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, force_authn: false, + request_signed: true, + matching_cert_serial: saml_test_sp_cert_serial, user_fully_authenticated: true, } ) diff --git a/spec/features/saml/saml_spec.rb b/spec/features/saml/saml_spec.rb index 73cb1f469d3..d0bee87d559 100644 --- a/spec/features/saml/saml_spec.rb +++ b/spec/features/saml/saml_spec.rb @@ -508,6 +508,8 @@ service_provider: 'http://localhost:3000', requested_aal_authn_context: Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, force_authn: false, + matching_cert_serial: saml_test_sp_cert_serial, + request_signed: true, user_fully_authenticated: false }], ) expect(fake_analytics.events['SAML Auth'].count).to eq 2 @@ -551,6 +553,8 @@ requested_ial: 'http://idmanagement.gov/ns/assurance/ial/2', service_provider: 'saml_sp_ial2', force_authn: false, + matching_cert_serial: saml_test_sp_cert_serial, + request_signed: true, user_fully_authenticated: false, }, ], @@ -581,6 +585,8 @@ service_provider: 'http://localhost:3000', requested_aal_authn_context: Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, force_authn: false, + matching_cert_serial: saml_test_sp_cert_serial, + request_signed: true, user_fully_authenticated: false }], ) expect(fake_analytics.events['SAML Auth'].count).to eq 2