From b640361eda317ae56bc780bca141d408d37103f3 Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Wed, 2 Aug 2023 10:11:25 -0700 Subject: [PATCH 1/4] fix(liveness): Added Rekognition backend for Android app and updated README (#59) Co-authored-by: Zuhayr Raghib Co-authored-by: Matt Creaser --- .../createSession/index.js | 23 ++++ .../createSession/package.json | 13 ++ .../getResults/index.js | 35 +++++ .../getResults/package.json | 13 ++ samples/liveness/README.md | 130 ++++++++++++++++-- 5 files changed, 203 insertions(+), 11 deletions(-) create mode 100644 samples/backend-lambda-functions/createSession/index.js create mode 100644 samples/backend-lambda-functions/createSession/package.json create mode 100644 samples/backend-lambda-functions/getResults/index.js create mode 100644 samples/backend-lambda-functions/getResults/package.json diff --git a/samples/backend-lambda-functions/createSession/index.js b/samples/backend-lambda-functions/createSession/index.js new file mode 100644 index 00000000..ff6502a3 --- /dev/null +++ b/samples/backend-lambda-functions/createSession/index.js @@ -0,0 +1,23 @@ +import { + RekognitionClient, + CreateFaceLivenessSessionCommand, +} from '@aws-sdk/client-rekognition'; + +/** + * @type {import('@types/aws-lambda').APIGatewayProxyHandler} + */ + +export const handler = async (event, req) => { + const client = new RekognitionClient({ region: 'us-east-1' }); + const command = new CreateFaceLivenessSessionCommand({}); + const response = await client.send(command); + + return { + statusCode: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': '*', + }, + body: JSON.stringify({ sessionId: response.SessionId }), + }; +}; diff --git a/samples/backend-lambda-functions/createSession/package.json b/samples/backend-lambda-functions/createSession/package.json new file mode 100644 index 00000000..4e45e64c --- /dev/null +++ b/samples/backend-lambda-functions/createSession/package.json @@ -0,0 +1,13 @@ +{ + "name": "create", + "version": "2.0.0", + "main": "index.js", + "license": "Apache-2.0", + "type": "module", + "dependencies": { + "@aws-sdk/client-rekognition": "latest" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.92" + } +} diff --git a/samples/backend-lambda-functions/getResults/index.js b/samples/backend-lambda-functions/getResults/index.js new file mode 100644 index 00000000..e59b93a7 --- /dev/null +++ b/samples/backend-lambda-functions/getResults/index.js @@ -0,0 +1,35 @@ +import { + RekognitionClient, + GetFaceLivenessSessionResultsCommand, +} from '@aws-sdk/client-rekognition'; + +/** + * @type {import('@types/aws-lambda').APIGatewayProxyHandler} + */ + +export const handler = async (event, req) => { + console.log({ req }); + console.log({ event }); + const client = new RekognitionClient({ region: 'us-east-1' }); + const command = new GetFaceLivenessSessionResultsCommand({ + SessionId: event.pathParameters.sessionId, + }); + const response = await client.send(command); + + const isLive = response.Confidence > 90; + + return { + statusCode: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': '*', + }, + body: JSON.stringify({ + isLive, + confidenceScore: response.Confidence, + auditImageBytes: Buffer.from( + new Uint8Array(Object.values(response.ReferenceImage.Bytes)) + ).toString('base64'), + }), + }; +}; \ No newline at end of file diff --git a/samples/backend-lambda-functions/getResults/package.json b/samples/backend-lambda-functions/getResults/package.json new file mode 100644 index 00000000..aeb0fb59 --- /dev/null +++ b/samples/backend-lambda-functions/getResults/package.json @@ -0,0 +1,13 @@ +{ + "name": "getresults", + "version": "2.0.0", + "main": "index.js", + "license": "Apache-2.0", + "type": "module", + "dependencies": { + "@aws-sdk/client-rekognition": "latest" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.92" + } +} diff --git a/samples/liveness/README.md b/samples/liveness/README.md index 1f4c8074..3a5ca183 100644 --- a/samples/liveness/README.md +++ b/samples/liveness/README.md @@ -26,15 +26,15 @@ amplify init Provide the responses shown after each of the following prompts. ``` ? Enter a name for the environment -`dev` + `dev` ? Choose your default editor: -`Android Studio` + `Android Studio` ? Where is your Res directory: -`app/src/main/res` + `app/src/main/res` ? Select the authentication method you want to use: -`AWS profile` + `AWS profile` ? Please choose the profile you want to use -`default` + `default` ``` Wait until provisioning is finished. Upon successfully running `amplify init`, you will see a configuration file created in `./app/src/main/res/raw/` called `amplifyconfiguration.json`. This file will be bundled into your application so that the Amplify libraries know how to reach your provisioned backend resources at runtime. @@ -65,12 +65,11 @@ Provide the responses shown after each of the following prompts. ? Select the social providers you want to configure for your user pool: `` ``` -4. Update the `AndroidManifest.xml` file in your project according to the steps [here](https://docs.amplify.aws/lib/auth/signin_web_ui/q/platform/android/#update-androidmanifestxml). -5. Once finished, run `amplify push` to publish your changes. +4. Once finished, run `amplify push` to publish your changes. Upon completion, `amplifyconfiguration.json` should be updated to reference these provisioned backend resources. -6. Follow the steps below to create an inline policy to enable authenticated app users to access Rekognition, which powers the FaceLivenessDetector. +5. Follow the steps below to create an inline policy to enable authenticated app users to access Rekognition, which powers the FaceLivenessDetector. 1. Go to AWS IAM console, then Roles - 2. Select the newly created `unauthRole` for the project (`amplify----authRole`). + 2. Select the newly created `authRole` for the project (`amplify----authRole`). 3. Choose **Add Permissions**, then select **Create Inline Policy**, then choose **JSON** and paste the following: ``` @@ -90,8 +89,117 @@ Provide the responses shown after each of the following prompts. 5. Name the policy 6. Choose **Create Policy** -7. Set up a backend to create the liveness session and retrieve the liveness session results. The liveness sample app is set up to use API Gateway endpoints for creating and retrieving the liveness session. Follow the [Amazon Rekognition Liveness guide](https://docs.aws.amazon.com/rekognition/latest/dg/face-liveness-programming-api.html) to set up your backend and edit the [LivenessSampleBackend class](https://github.com/aws-amplify/amplify-ui-android/blob/main/samples/liveness/app/src/main/java/com/amplifyframework/ui/sample/liveness/LivenessSampleBackend.kt) in your project as necessary to work with your backend. +### Provision Backend API +Set up a backend API using [Amplify API category](https://docs.amplify.aws/lib/restapi/getting-started/q/platform/android/) to create the liveness session and retrieve the liveness session results. The liveness sample app is set up to use API Gateway endpoints for creating and retrieving the liveness session. +1. Run the following command to create a new REST API: +``` +amplify add api +``` +Provide the responses shown after each of the following prompts. +``` +? Please select from one of the below mentioned services + `REST` +? Would you like to add a new path to an existing REST API: + `N` +? Provide a friendly name for your resource to be used as a label for this category in the project: + `livenessBackendAPI` +? Provide a path (e.g., /book/{isbn}): + `/liveness/create` +? Choose a Lambda source + `Create a new Lambda function` +? Provide an AWS Lambda function name: + `createSession` +? Choose the runtime that you want to use: + `NodeJS` +? Choose the function template that you want to use: + `Serverless ExpressJS function (Integration with API Gateway)` +? Do you want to configure advanced settings? + `N` +? Do you want to edit the local lambda function now? + `Y` +? Restrict API access? + `Y` +? Who should have access? + `N` +? Do you want to configure advanced settings? + `Authenticated users only` +? What permissions do you want to grant to Authenticated users? + `create,read,update` +? Do you want to add another path? + `Y` +? Provide a path (e.g., /book/{isbn}): + `/liveness/{sessionId}` +? Choose a Lambda source + `Create a new Lambda function` +? Provide an AWS Lambda function name: + `getResults` +? Choose the runtime that you want to use: + `NodeJS` +? Choose the function template that you want to use: + `Serverless ExpressJS function (Integration with API Gateway)` +? Do you want to configure advanced settings? + `N` +? Do you want to edit the local lambda function now? + `Y` +? Restrict API access? + `Y` +? Who should have access? + `Authenticated users only` +? What permissions do you want to grant to Authenticated users? + `create,read,update` +? Do you want to add another path? + `N` +``` +2. Copy the code for from amplify-ui-android/samples/backend-lambda-functions to the path provided +3. Once finished, run `amplify push` to publish your changes. +4. Follow the steps below to create an inline policy to enable the **createSession** lambda function to access Rekognition. + 1. Go to AWS Lambda console -> **CreateSession** -> Configuration -> Permissions + 2. Click the role name under 'Execution role' + 3. Choose **Add Permissions**, then select **Create Inline Policy**, then choose **JSON** and paste the following: + + ``` + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "rekognition:CreateFaceLivenessSession", + "Resource": "*" + } + ] + } + ``` + 4. Choose **Review Policy** + 5. Name the policy + 6. Choose **Create Policy** +5. Follow the steps below to create an inline policy to enable the **getResults** lambda function to access Rekognition. + 1. Go to AWS Lambda console -> **getResults** -> Configuration -> Permissions + 2. Click the role name under 'Execution role' + 3. Choose **Add Permissions**, then select **Create Inline Policy**, then choose **JSON** and paste the following: + + ``` + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "rekognition:GetFaceLivenessSessionResults", + "Resource": "*" + } + ] + } + ``` + 4. Choose **Review Policy** + 5. Name the policy + 6. Choose **Create Policy** ### Run the App -Build and run the project on an Android device in Android Studio. The project requires Android SDK API level 24 (Android 7.0) or higher. +Delete the generated API files app/src/main/java/[YOUR_API_NAME] + +You may need to go to File -> Sync Project with Gradle Files if you get an error "SDK location not found". + +Build and run the project on an Android device in Android Studio. + + +The project requires Android SDK API level 24 (Android 7.0) or higher. From 5d469c1fe0044349855d955d2fcdc62fcdf4af24 Mon Sep 17 00:00:00 2001 From: Thomas Leing Date: Thu, 3 Aug 2023 10:13:00 -0700 Subject: [PATCH 2/4] fix(liveness): Screen rotation saves/loads correctly; lifecycle not destroyed at time of use (#65) Co-authored-by: Thomas Leing --- .../ui/liveness/camera/LivenessCoordinator.kt | 31 ++++++----- .../ui/liveness/ui/FaceLivenessDetector.kt | 55 ++++++++++--------- .../ui/liveness/ui/LockPortraitOrientation.kt | 20 +++++-- 3 files changed, 59 insertions(+), 47 deletions(-) diff --git a/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt b/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt index ed746a8f..9bef04f7 100644 --- a/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt +++ b/liveness/src/main/java/com/amplifyframework/ui/liveness/camera/LivenessCoordinator.kt @@ -26,6 +26,7 @@ import androidx.camera.core.ImageAnalysis import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import com.amplifyframework.auth.AWSCredentials import com.amplifyframework.auth.AWSCredentialsProvider @@ -136,20 +137,22 @@ internal class LivenessCoordinator( init { MainScope().launch { getCameraProvider(context).apply { - unbindAll() - if (this.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA)) { - bindToLifecycle( - lifecycleOwner, - CameraSelector.DEFAULT_FRONT_CAMERA, - preview, - analysis - ) - } else { - val faceLivenessException = FaceLivenessDetectionException( - "A front facing camera is required but no front facing camera detected.", - "Enable a front facing camera." - ) - processSessionError(faceLivenessException, true) + if (lifecycleOwner.lifecycle.currentState != Lifecycle.State.DESTROYED) { + unbindAll() + if (this.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA)) { + bindToLifecycle( + lifecycleOwner, + CameraSelector.DEFAULT_FRONT_CAMERA, + preview, + analysis + ) + } else { + val faceLivenessException = FaceLivenessDetectionException( + "A front facing camera is required but no front facing camera detected.", + "Enable a front facing camera." + ) + processSessionError(faceLivenessException, true) + } } } } diff --git a/liveness/src/main/java/com/amplifyframework/ui/liveness/ui/FaceLivenessDetector.kt b/liveness/src/main/java/com/amplifyframework/ui/liveness/ui/FaceLivenessDetector.kt index 2d1e43d0..c61ef898 100644 --- a/liveness/src/main/java/com/amplifyframework/ui/liveness/ui/FaceLivenessDetector.kt +++ b/liveness/src/main/java/com/amplifyframework/ui/liveness/ui/FaceLivenessDetector.kt @@ -109,33 +109,35 @@ fun FaceLivenessDetector( } // Locks portrait orientation for duration of challenge and resets on complete - LockPortraitOrientation() - - Surface(color = MaterialTheme.colorScheme.background) { - if (showReadyView) { - GetReadyView { - showReadyView = false - } - } else { - AlwaysOnMaxBrightnessScreen() - ChallengeView( - key = key, - sessionId = sessionId, - region, - credentialsProvider = credentialsProvider, - onChallengeComplete = { - scope.launch { - isFinished = true - currentOnComplete.call() - } - }, - onChallengeFailed = { - scope.launch { - isFinished = true - currentOnError.accept(it) - } + LockPortraitOrientation { resetOrientation -> + Surface(color = MaterialTheme.colorScheme.background) { + if (showReadyView) { + GetReadyView { + showReadyView = false } - ) + } else { + AlwaysOnMaxBrightnessScreen() + ChallengeView( + key = key, + sessionId = sessionId, + region, + credentialsProvider = credentialsProvider, + onChallengeComplete = { + scope.launch { + isFinished = true + resetOrientation() + currentOnComplete.call() + } + }, + onChallengeFailed = { + scope.launch { + isFinished = true + resetOrientation() + currentOnError.accept(it) + } + } + ) + } } } } @@ -191,7 +193,6 @@ internal fun ChallengeView( ) } ) { - val videoViewportSize = livenessState.videoViewportSize if (videoViewportSize != null) { diff --git a/liveness/src/main/java/com/amplifyframework/ui/liveness/ui/LockPortraitOrientation.kt b/liveness/src/main/java/com/amplifyframework/ui/liveness/ui/LockPortraitOrientation.kt index 75c2fecc..de422150 100644 --- a/liveness/src/main/java/com/amplifyframework/ui/liveness/ui/LockPortraitOrientation.kt +++ b/liveness/src/main/java/com/amplifyframework/ui/liveness/ui/LockPortraitOrientation.kt @@ -17,20 +17,28 @@ package com.amplifyframework.ui.liveness.ui import android.annotation.SuppressLint import android.content.pm.ActivityInfo +import android.content.res.Configuration.ORIENTATION_PORTRAIT import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.platform.LocalContext import com.amplifyframework.ui.liveness.util.findActivity @SuppressLint("SourceLockedOrientationActivity") @Composable -internal fun LockPortraitOrientation() { +internal fun LockPortraitOrientation(content: @Composable (resetOrientation: () -> Unit) -> Unit) { val context = LocalContext.current - DisposableEffect(Unit) { - val activity = context.findActivity() ?: return@DisposableEffect onDispose {} - val originalOrientation = activity.requestedOrientation + val activity = context.findActivity() ?: return content {} + val originalOrientation by rememberSaveable { mutableStateOf(activity.requestedOrientation) } + SideEffect { activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - onDispose { + } + + // wait until screen is rotated correctly + if (activity.resources.configuration.orientation == ORIENTATION_PORTRAIT) { + content { activity.requestedOrientation = originalOrientation } } From a4269242a430fa2086f6996e0a9f21d33ade0235 Mon Sep 17 00:00:00 2001 From: gpanshu <91897496+gpanshu@users.noreply.github.com> Date: Thu, 3 Aug 2023 15:38:40 -0500 Subject: [PATCH 3/4] release liveness 1.1.2 (#71) Co-authored-by: Thomas Leing --- README.md | 4 ++-- liveness/CHANGELOG.md | 9 +++++++++ liveness/gradle.properties | 2 +- samples/liveness/build.gradle | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f3957a41..5b6efc9c 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,14 @@ Amplify UI for Android is an open-source UI library with cloud-connected compone | Component | Summary | Latest Version | Docs | Sample | | --- | --- |------------------------------------------------------------------------------------------------------| --- | --- | | [Authenticator](authenticator) | Amplify Authenticator provides a complete drop-in implementation of an authentication flow for your application using [Amplify Authentication](https://docs.amplify.aws/lib/auth/getting-started/q/platform/android/). | [1.0.0](https://github.com/aws-amplify/amplify-ui-android/releases/tag/release_authenticator_v1.0.0) | [Docs](https://ui.docs.amplify.aws/android/connected-components/authenticator) | [Sample](samples/authenticator/) | -| [Face Liveness](liveness) | Amplify FaceLivenessDetector provides a UI component for [Amazon Rekognition Face Liveness](https://aws.amazon.com/rekognition/face-liveness/) feature that helps developers verify that only real users, not bad actors using spoofs, can access your services. | [1.1.1](https://github.com/aws-amplify/amplify-ui-android/releases/tag/release_liveness_v1.1.1) | [Docs](https://ui.docs.amplify.aws/android/connected-components/liveness) | [Sample](samples/liveness/) | +| [Face Liveness](liveness) | Amplify FaceLivenessDetector provides a UI component for [Amazon Rekognition Face Liveness](https://aws.amazon.com/rekognition/face-liveness/) feature that helps developers verify that only real users, not bad actors using spoofs, can access your services. | [1.1.2](https://github.com/aws-amplify/amplify-ui-android/releases/tag/release_liveness_v1.1.2) | [Docs](https://ui.docs.amplify.aws/android/connected-components/liveness) | [Sample](samples/liveness/) | ## Supported Versions | Component | Version | Amplify | Material3 | | --- |---------|---------|-----------| | Authenticator | 1.0.0 | 2.8.4+ | 1.1.0 | -| Liveness | 1.1.1 | 2.11.1+ | 1.1.0 | +| Liveness | 1.1.2 | 2.11.1+ | 1.1.0 | ## Getting Started diff --git a/liveness/CHANGELOG.md b/liveness/CHANGELOG.md index 6ece6f12..733c383f 100644 --- a/liveness/CHANGELOG.md +++ b/liveness/CHANGELOG.md @@ -1,3 +1,12 @@ +## [Release 1.1.2](https://github.com/aws-amplify/amplify-ui-android/releases/tag/release_liveness_v1.1.2) +## Bug Fixes +* Update Amplify min version for Liveness in https://github.com/aws-amplify/amplify-ui-android/pull/68 +* Update build.gradle in https://github.com/aws-amplify/amplify-ui-android/pull/69 +* fix(liveness): Added Rekognition backend for Android app and updated README in https://github.com/aws-amplify/amplify-ui-android/pull/59 +* fix(liveness): Screen rotation saves/loads correctly; lifecycle not destroyed at time of use in https://github.com/aws-amplify/amplify-ui-android/pull/65 + +[See all changes between 1.1.1 and 1.1.2](https://github.com/aws-amplify/amplify-ui-android/compare/release_liveness_v1.1.1...release_liveness_v1.1.2) + ## [Release 1.1.1](https://github.com/aws-amplify/amplify-ui-android/releases/tag/release_liveness_v1.1.1) ### Features diff --git a/liveness/gradle.properties b/liveness/gradle.properties index c4ea1220..dd056341 100644 --- a/liveness/gradle.properties +++ b/liveness/gradle.properties @@ -17,4 +17,4 @@ POM_ARTIFACT_ID=liveness POM_NAME=Amplify UI Framework for Android - Liveness POM_DESCRIPTION=Amplify UI Framework for Android - Liveness Plugin POM_PACKAGING=aar -VERSION_NAME=1.1.1 +VERSION_NAME=1.1.2 diff --git a/samples/liveness/build.gradle b/samples/liveness/build.gradle index 93870b88..3dccddb5 100644 --- a/samples/liveness/build.gradle +++ b/samples/liveness/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { compose_version = '1.3.3' amplifyVersion = '2.11.1' - amplifyUIVersion = '1.1.1' + amplifyUIVersion = '1.1.2' } repositories { google() From a06a6a739be0573e9e01eba53f7b7f8b9576e5b0 Mon Sep 17 00:00:00 2001 From: Ankit Shah <22114629+ankpshah@users.noreply.github.com> Date: Wed, 9 Aug 2023 10:27:46 -0700 Subject: [PATCH 4/4] chore: pin github actions by commit hash (#72) --- .github/workflows/ci-unit-tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-unit-tests.yml b/.github/workflows/ci-unit-tests.yml index 9f5f0d4b..16f17060 100644 --- a/.github/workflows/ci-unit-tests.yml +++ b/.github/workflows/ci-unit-tests.yml @@ -19,13 +19,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Source Code - uses: actions/checkout@v3 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3 - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v2 + uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2 with: role-to-assume: ${{ vars.AMPLIFY_UI_ANDROID_CI_TESTS_ROLE }} aws-region: ${{ env.AWS_REGION }} - name: Run Unit Tests - uses: aws-actions/aws-codebuild-run-build@v1 + uses: aws-actions/aws-codebuild-run-build@f202c327329cbbebd13f986f74af162a8539b5fd # v1 with: project-name: Amplify-UI-Android-Build