diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt index 85e0473df9..037ee0cca6 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/http/LivenessWebSocket.kt @@ -37,6 +37,7 @@ import com.amplifyframework.predictions.aws.models.liveness.ColorDisplayed import com.amplifyframework.predictions.aws.models.liveness.FaceMovementAndLightClientChallenge import com.amplifyframework.predictions.aws.models.liveness.FreshnessColor import com.amplifyframework.predictions.aws.models.liveness.InitialFace +import com.amplifyframework.predictions.aws.models.liveness.InvalidSignatureException import com.amplifyframework.predictions.aws.models.liveness.LivenessResponseStream import com.amplifyframework.predictions.aws.models.liveness.SessionInformation import com.amplifyframework.predictions.aws.models.liveness.TargetFace @@ -78,11 +79,11 @@ internal class LivenessWebSocket( private val signer = AWSV4Signer() private var credentials: Credentials? = null - internal var offset = 0L + // The reported time difference between the server and client. Only set if diff is higher than 4 minutes + internal var timeDiffOffsetInMillis = 0L internal enum class ConnectionState { NORMAL, ATTEMPT_RECONNECT, - TOO_MANY_RECONNECTS; } internal var reconnectState = ConnectionState.NORMAL @@ -111,21 +112,13 @@ internal class LivenessWebSocket( super.onOpen(webSocket, response) - // if offset is > 5 minutes, server will reject the request - if (kotlin.math.abs(tempOffset) < FIVE_MINUTES) { - reconnectState = ConnectionState.NORMAL - this@LivenessWebSocket.webSocket = webSocket - } else { - // server will close this websocket - if (reconnectState == ConnectionState.ATTEMPT_RECONNECT) { - // this is not the first try, report that failure back - reconnectState = ConnectionState.TOO_MANY_RECONNECTS - } else { - // this is the first try, don't report that failure back - reconnectState = ConnectionState.ATTEMPT_RECONNECT - offset = tempOffset - start() - } + this@LivenessWebSocket.webSocket = webSocket + + // If offset is > 4 minutes, server may reject the request + // The real allowed diff from serer is < 5 but we check for 4 to add a buffer + if (!isTimeDiffSafe(tempOffset)) { + LOG.info("Server reported a time difference between client and server of > 4 minutes") + timeDiffOffsetInMillis = tempOffset } } @@ -167,10 +160,22 @@ internal class LivenessWebSocket( override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { LOG.debug("WebSocket onClosed") super.onClosed(webSocket, code, reason) - if (reconnectState == ConnectionState.ATTEMPT_RECONNECT) { - // do nothing; we expected the server to close the connection + val recordedError = webSocketError + /* + If the server reports an invalid signature due to a time difference between the local clock and the + server clock, AND we haven't already tried to reconnect, then we should try to reconnect with an offset + */ + if (reconnectState == ConnectionState.NORMAL && + !isTimeDiffSafe(timeDiffOffsetInMillis) && + recordedError is PredictionsException && + recordedError.cause is InvalidSignatureException + ) { + LOG.info("The server rejected the connection due to a likely time difference. Attempting reconnect.") + reconnectState = ConnectionState.ATTEMPT_RECONNECT + webSocketError = null + start() } else if (code != NORMAL_SOCKET_CLOSURE_STATUS_CODE && !clientStoppedSession) { - val faceLivenessException = webSocketError ?: PredictionsException( + val faceLivenessException = recordedError ?: PredictionsException( "An error occurred during the face liveness check.", reason ) @@ -302,6 +307,18 @@ internal class LivenessWebSocket( AccessDeniedException( cause = livenessResponse.accessDeniedException ) + } else if (livenessResponse.unrecognizedClientException != null) { + PredictionsException( + "Unrecognized client", + livenessResponse.unrecognizedClientException, + "Please check your credentials" + ) + } else if (livenessResponse.invalidSignatureException != null) { + PredictionsException( + "Invalid signature", + livenessResponse.invalidSignatureException, + "Please check your credentials" + ) } else { PredictionsException( "An unknown error occurred during the Liveness flow.", @@ -467,12 +484,14 @@ internal class LivenessWebSocket( } fun adjustedDate(date: Long = Date().time): Long { - return date + offset + return date + timeDiffOffsetInMillis } + private fun isTimeDiffSafe(diffInMillis: Long) = kotlin.math.abs(diffInMillis) < FOUR_MINUTES + companion object { private const val NORMAL_SOCKET_CLOSURE_STATUS_CODE = 1000 - private val FIVE_MINUTES = 1000 * 60 * 5 + private const val FOUR_MINUTES = 1000 * 60 * 4 @VisibleForTesting val datePattern = "EEE, d MMM yyyy HH:mm:ss z" private val LOG = Amplify.Logging.logger(CategoryType.PREDICTIONS, "amplify:aws-predictions") } diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt index 0d9c6e60d8..5485efd678 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/LivenessResponseStream.kt @@ -30,5 +30,6 @@ internal data class LivenessResponseStream( @SerialName("ServiceUnavailableException") val serviceUnavailableException: ServiceUnavailableException? = null, @SerialName("SessionNotFoundException") val sessionNotFoundException: SessionNotFoundException? = null, @SerialName("AccessDeniedException") val accessDeniedException: AccessDeniedException? = null, - @SerialName("InvalidSignatureException") val invalidSignatureException: InvalidSignatureException? = null + @SerialName("InvalidSignatureException") val invalidSignatureException: InvalidSignatureException? = null, + @SerialName("UnrecognizedClientException") val unrecognizedClientException: UnrecognizedClientException? = null ) diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/UnrecognizedClientException.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/UnrecognizedClientException.kt new file mode 100644 index 0000000000..7d0b61ddb1 --- /dev/null +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/models/liveness/UnrecognizedClientException.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amplifyframework.predictions.aws.models.liveness + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Constructs a new UnrecognizedClientException with the specified error message. + * + * @param message Describes the error encountered. + */ +@Serializable +internal data class UnrecognizedClientException( + @SerialName("Message") override val message: String +) : Exception(message)