Skip to content

Commit

Permalink
PYIC-7076: Add check-reverification-identity lambda (#2751)
Browse files Browse the repository at this point in the history
  • Loading branch information
Wynndow authored Dec 10, 2024
2 parents 110b5db + c042832 commit c5fc91f
Show file tree
Hide file tree
Showing 33 changed files with 1,557 additions and 490 deletions.
138 changes: 73 additions & 65 deletions api-tests/features/mfa-reset-journey.feature
Original file line number Diff line number Diff line change
@@ -1,75 +1,83 @@
@Build
Feature: MFA reset journey
Background: There is an existing user and they start an MFA reset journey
Given the subject already has the following credentials
| CRI | scenario |
| dcmaw | kenneth-driving-permit-valid |
| address | kenneth-current |
| fraud | kenneth-score-2 |
Rule: User has an existing identity
Background: There is an existing user and they start an MFA reset journey
Given the subject already has the following credentials
| CRI | scenario |
| dcmaw | kenneth-driving-permit-valid |
| address | kenneth-current |
| fraud | kenneth-score-2 |

# Start MFA reset journey
When I start a new 'reverification' journey
Then I get a 'page-ipv-identity-document-start' page response
# Start MFA reset journey
When I start a new 'reverification' journey
Then I get a 'page-ipv-identity-document-start' page response

Scenario: Successful MFA reset journey
When I submit an 'appTriage' event
Then I get a 'dcmaw' CRI response
When I submit 'kenneth-driving-permit-valid' details to the CRI stub
Then I get a 'page-dcmaw-success' page response
When I submit a 'next' event
Then I get an OAuth response
When I use the OAuth response to get my MFA reset result
Then I get a successful MFA reset result
Scenario: Successful MFA reset journey
When I submit an 'appTriage' event
Then I get a 'dcmaw' CRI response
When I submit 'kenneth-driving-permit-valid' details to the CRI stub
Then I get a 'page-dcmaw-success' page response
When I submit a 'next' event
Then I get an OAuth response
When I use the OAuth response to get my MFA reset result
Then I get a successful MFA reset result

Scenario: Failed MFA reset journey with breaching CI - user can still reuse existing identity
When I submit an 'appTriage' event
Then I get a 'dcmaw' CRI response
When I submit 'kenneth-passport-with-breaching-ci' details to the CRI stub
Then I get a 'pyi-no-match' page response
When I submit a 'next' event
Then I get an OAuth response
When I use the OAuth response to get my MFA reset result
Then I get an unsuccessful MFA reset result with failure code 'identity_check_failed'
Scenario: Failed MFA reset journey with breaching CI - user can still reuse existing identity
When I submit an 'appTriage' event
Then I get a 'dcmaw' CRI response
When I submit 'kenneth-passport-with-breaching-ci' details to the CRI stub
Then I get a 'pyi-no-match' page response
When I submit a 'next' event
Then I get an OAuth response
When I use the OAuth response to get my MFA reset result
Then I get an unsuccessful MFA reset result with failure code 'identity_check_failed'

# New journey with same user id
When I start a new 'medium-confidence' journey
Then I get a 'page-ipv-reuse' page response
# New journey with same user id
When I start a new 'medium-confidence' journey
Then I get a 'page-ipv-reuse' page response

Scenario: Failed MFA reset journey - DCMAW error
When I submit an 'appTriage' event
Then I get a 'dcmaw' CRI response
When I call the CRI stub and get an 'access-denied' OAuth error
When I submit a 'next' event
Then I get an OAuth response
When I use the OAuth response to get my MFA reset result
Then I get an unsuccessful MFA reset result with failure code 'identity_check_incomplete'
Scenario: Failed MFA reset journey - DCMAW error
When I submit an 'appTriage' event
Then I get a 'dcmaw' CRI response
When I call the CRI stub and get an 'access-denied' OAuth error
When I submit a 'next' event
Then I get an OAuth response
When I use the OAuth response to get my MFA reset result
Then I get an unsuccessful MFA reset result with failure code 'identity_check_incomplete'

Scenario: Failed MFA reset journey - no photo id
When I submit an 'end' event
Then I get a 'pyi-another-way' page response
When I submit an 'next' event
Then I get an OAuth response
When I use the OAuth response to get my MFA reset result
Then I get an unsuccessful MFA reset result with failure code 'identity_check_incomplete'
Scenario: Failed MFA reset journey - no photo id
When I submit an 'end' event
Then I get a 'pyi-another-way' page response
When I submit an 'next' event
Then I get an OAuth response
When I use the OAuth response to get my MFA reset result
Then I get an unsuccessful MFA reset result with failure code 'identity_check_incomplete'

Scenario: Failed MFA reset journey - failed verification score
When I submit an 'appTriage' event
Then I get a 'dcmaw' CRI response
When I submit 'kenneth-passport-verification-zero' details to the CRI stub
Then I get a 'pyi-no-match' page response
When I submit a 'next' event
Then I get an OAuth response
When I use the OAuth response to get my MFA reset result
Then I get an unsuccessful MFA reset result with failure code 'identity_check_failed'
Scenario: Failed MFA reset journey - failed verification score
When I submit an 'appTriage' event
Then I get a 'dcmaw' CRI response
When I submit 'kenneth-passport-verification-zero' details to the CRI stub
Then I get a 'pyi-no-match' page response
When I submit a 'next' event
Then I get an OAuth response
When I use the OAuth response to get my MFA reset result
Then I get an unsuccessful MFA reset result with failure code 'identity_check_failed'

Scenario: Failed MFA reset journey - non-matching identity
When I submit an 'appTriage' event
Then I get a 'dcmaw' CRI response
When I submit 'alice-passport-valid' details to the CRI stub
Then I get a 'page-dcmaw-success' page response
When I submit a 'next' event
Then I get a 'pyi-no-match' page response
When I submit a 'next' event
Then I get an OAuth response
When I use the OAuth response to get my MFA reset result
Then I get an unsuccessful MFA reset result with failure code 'identity_did_not_match'
Scenario: Failed MFA reset journey - non-matching identity
When I submit an 'appTriage' event
Then I get a 'dcmaw' CRI response
When I submit 'alice-passport-valid' details to the CRI stub
Then I get a 'page-dcmaw-success' page response
When I submit a 'next' event
Then I get a 'pyi-no-match' page response
When I submit a 'next' event
Then I get an OAuth response
When I use the OAuth response to get my MFA reset result
Then I get an unsuccessful MFA reset result with failure code 'identity_did_not_match'

Rule: The user has no existing identity
Scenario: Attempted MFA reset journey
When I start a new 'reverification' journey
Then I get an OAuth response
When I use the OAuth response to get my MFA reset result
Then I get an unsuccessful MFA reset result with failure code 'no_identity_available'
21 changes: 21 additions & 0 deletions deploy/journeyEngineStepFunction.asl.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@
"Variable": "$.journey",
"StringMatches": "/journey/call-dcmaw-async-cri",
"Next": "CallDcmawAsyncCriLambda"
},
{
"Variable": "$.journey",
"StringMatches": "/journey/check-reverification-identity",
"Next": "CheckReverificationIdentityLambda"
}
],
"Default": "Success"
Expand Down Expand Up @@ -278,6 +283,22 @@
"MaxDelaySeconds": 4
}]
},
"CheckReverificationIdentityLambda": {
"Type": "Task",
"Resource": "${CheckReverificationIdentityFunctionArn}",
"Parameters": {
"ipvSessionId.$": "$$.Execution.Input.ipvSessionId",
"featureSet.$": "$$.Execution.Input.featureSet"
},
"Next": "ProcessNextJourney",
"Retry": [{
"ErrorEquals": ["Lambda.SnapStartNotReadyException"],
"IntervalSeconds": 1,
"BackoffRate": 2,
"MaxAttempts": 5,
"MaxDelaySeconds": 4
}]
},
"ProcessNextJourney": {
"Type": "Choice",
"Choices": [
Expand Down
97 changes: 97 additions & 0 deletions deploy/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1511,6 +1511,7 @@ Resources:
StoreIdentityLambdaArn: !Ref StoreIdentityFunction.Version
ResetSessionIdentityFunctionArn: !Ref ResetSessionIdentityFunction.Version
CheckCoiFunctionArn: !Ref CheckCoiFunction.Version
CheckReverificationIdentityFunctionArn: !Ref CheckReverificationIdentityFunction.Version
AutoPublishAlias: live
DeploymentPreference:
Type: !Ref StepFunctionDeploymentPreference
Expand All @@ -1531,6 +1532,7 @@ Resources:
- !Ref StoreIdentityFunctionErrorCanaryAlarm
- !Ref ResetSessionIdentityFunctionErrorCanaryAlarm
- !Ref CheckCoiFunctionErrorCanaryAlarm
- !Ref CheckReverificationIdentityFunctionErrorCanaryAlarm
- !Ref AWS::NoValue
StateMachineVersionArn: !Sub "arn:aws:states:${AWS::Region}:${AWS::AccountId}:stateMachine:${JourneyEngineStepFunction}:live"
Events:
Expand Down Expand Up @@ -1576,6 +1578,8 @@ Resources:
FunctionName: !Ref ResetSessionIdentityFunction
- LambdaInvokePolicy:
FunctionName: !Ref CheckCoiFunction
- LambdaInvokePolicy:
FunctionName: !Ref CheckReverificationIdentityFunction
- Statement:
- Sid: CloudWatchLogsAccess
Effect: Allow
Expand Down Expand Up @@ -2604,6 +2608,73 @@ Resources:
FilterPattern: ""
LogGroupName: !Ref CheckCoiFunctionLogGroup

CheckReverificationIdentityFunction:
Type: AWS::Serverless::Function
DependsOn:
- CheckReverificationIdentityFunctionLogGroup
Properties:
# checkov:skip=CKV_AWS_115: We do not have enough data to allocate the concurrent execution allowance per function.
# checkov:skip=CKV_AWS_116: Lambdas invoked via API Gateway do not support Dead Letter Queues.
# checkov:skip=CKV_AWS_117: Lambdas will migrate to our own VPC in future work.
FunctionName: !Sub "check-reverification-identity-${Environment}"
Handler: uk.gov.di.ipv.core.checkreverificationidentity.CheckReverificationIdentityHandler::handleRequest
PackageType: Zip
CodeUri: ../lambdas/check-reverification-identity
Tracing: Active
Environment:
# checkov:skip=CKV_AWS_173: These environment variables do not require encryption.
Variables:
ENVIRONMENT: !Sub "${Environment}"
POWERTOOLS_SERVICE_NAME: !Sub check-reverification-identity-${Environment}
IPV_SESSIONS_TABLE_NAME: !Ref SessionsTable
CLIENT_OAUTH_SESSIONS_TABLE_NAME: !Ref ClientOAuthSessionsTable
VpcConfig:
SubnetIds:
- Fn::ImportValue: !Sub ${VpcStackName}-ProtectedSubnetIdA
- Fn::ImportValue: !Sub ${VpcStackName}-ProtectedSubnetIdB
SecurityGroupIds:
- !GetAtt LambdaSecurityGroup.GroupId
Policies:
- VPCAccessPolicy: { }
- Statement:
- Sid: EnforceStayinSpecificVpc
Effect: Allow
Action:
- 'lambda:CreateFunction'
- 'lambda:UpdateFunctionConfiguration'
Resource:
- "*"
Condition:
StringEquals:
"lambda:VpcIds":
- Fn::ImportValue: !Sub ${VpcStackName}-VpcId
- KMSDecryptPolicy:
KeyId: !Ref DynamoDBKmsKey
- SSMParameterReadPolicy:
ParameterName: !Sub ${Environment}/core/*
- DynamoDBCrudPolicy:
TableName: !Ref SessionsTable
- DynamoDBReadPolicy:
TableName: !Ref ClientOAuthSessionsTable
- AWSSecretsManagerGetSecretValuePolicy:
SecretArn: !Sub arn:aws:secretsmanager:eu-west-2:*:secret:/${Environment}/core/evcs/apiKey-*
AutoPublishAlias: live

CheckReverificationIdentityFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
RetentionInDays: 30
LogGroupName: !Sub "/aws/lambda/check-reverification-identity-${Environment}"
KmsKeyId: !GetAtt LoggingKmsKey.Arn

CheckReverificationIdentityFunctionLogGroupSubscriptionFilter:
Type: AWS::Logs::SubscriptionFilter
Condition: IsSubscriptionEnviroment
Properties:
DestinationArn: "arn:aws:logs:eu-west-2:885513274347:destination:csls_cw_logs_destination_prodpython"
FilterPattern: ""
LogGroupName: !Ref CheckReverificationIdentityFunctionLogGroup

UserIssuedCredentialsV2Table:
Type: AWS::DynamoDB::Table
Properties:
Expand Down Expand Up @@ -3721,6 +3792,32 @@ Resources:
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching

CheckReverificationIdentityFunctionErrorCanaryAlarm:
Type: AWS::CloudWatch::Alarm
Condition: UseCanaryDeploymentAlarms
Properties:
ActionsEnabled: true
AlarmActions:
- !ImportValue alarm-alerts-topic
AlarmDescription: !Sub "Error returned from the CheckReverificationIdentityFunction"
AlarmName: !Sub ${AWS::StackName}-CheckReverificationIdentityFunction-ErrorCanary
MetricName: Errors
Dimensions:
- Name: Resource
Value: !Sub "check-reverification-identity-${Environment}:live"
- Name: FunctionName
Value: !Ref CheckReverificationIdentityFunction
- Name: ExecutedVersion
Value: !GetAtt CheckReverificationIdentityFunction.Version.Version
Namespace: AWS/Lambda
Statistic: Sum
Unit: Count
Period: 60
EvaluationPeriods: 3
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching

Outputs:
IPVCorePrivateAPIGatewayID:
Description: Core Back Private API Gateway ID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand All @@ -72,6 +73,7 @@
import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_TO_PARSE_EVIDENCE_REQUESTED;
import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_TO_PARSE_ISSUED_CREDENTIALS;
import static uk.gov.di.ipv.core.library.domain.ErrorResponse.IPV_SESSION_NOT_FOUND;
import static uk.gov.di.ipv.core.library.domain.ErrorResponse.MISSING_TARGET_VOT;
import static uk.gov.di.ipv.core.library.domain.EvidenceRequest.SCORING_POLICY_GPG45;
import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_LAMBDA_RESULT;
import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_REDIRECT_URI;
Expand Down Expand Up @@ -180,8 +182,6 @@ public Map<String, Object> handleRequest(CriJourneyRequest input, Context contex

String govukSigninJourneyId = clientOAuthSessionItem.getGovukSigninJourneyId();

Vot targetVot = ipvSessionItem.getTargetVot();

LogHelper.attachGovukSigninJourneyIdToLogs(govukSigninJourneyId);

String oauthState = SecureTokenHelper.getInstance().generate();
Expand All @@ -194,8 +194,7 @@ public Map<String, Object> handleRequest(CriJourneyRequest input, Context contex
govukSigninJourneyId,
cri,
criContext,
criEvidenceRequest,
targetVot);
criEvidenceRequest);

CriResponse criResponse = getCriResponse(criConfig, jweObject, cri, language);

Expand Down Expand Up @@ -314,8 +313,7 @@ private JWEObject signEncryptJar(
String govukSigninJourneyId,
Cri cri,
String context,
EvidenceRequest evidenceRequest,
Vot requestedVot)
EvidenceRequest evidenceRequest)
throws HttpResponseExceptionWithErrorBody, ParseException, JOSEException,
VerifiableCredentialException {

Expand All @@ -327,9 +325,24 @@ private JWEObject signEncryptJar(
ipvSessionItem.getEmailAddress(), vcs, getAllowedSharedClaimAttrs(cri));

if (cri.equals(F2F)) {
evidenceRequest = getEvidenceRequestForF2F(vcs, requestedVot);
evidenceRequest =
getEvidenceRequestForF2F(
vcs,
Optional.ofNullable(ipvSessionItem.getTargetVot())
.orElseThrow(
() ->
new HttpResponseExceptionWithErrorBody(
SC_INTERNAL_SERVER_ERROR,
MISSING_TARGET_VOT)));
} else if (cri.isKbvCri()) {
evidenceRequest = getEvidenceRequestForKbvCri(ipvSessionItem.getTargetVot());
evidenceRequest =
getEvidenceRequestForKbvCri(
Optional.ofNullable(ipvSessionItem.getTargetVot())
.orElseThrow(
() ->
new HttpResponseExceptionWithErrorBody(
SC_INTERNAL_SERVER_ERROR,
MISSING_TARGET_VOT)));
}
SignedJWT signedJWT =
AuthorizationRequestHelper.createSignedJWT(
Expand Down
Loading

0 comments on commit c5fc91f

Please sign in to comment.