diff --git a/.github/workflows/bitbar-prepare-artifacts.yaml b/.github/workflows/bitbar-prepare-artifacts.yaml index 28e14205..e68585c2 100644 --- a/.github/workflows/bitbar-prepare-artifacts.yaml +++ b/.github/workflows/bitbar-prepare-artifacts.yaml @@ -46,11 +46,11 @@ jobs: - name: List build tools versions run: ls /Users/runner/Library/Android/sdk/build-tools/ - # Sign auth-debug-androidTest.apk - - name: Sign auth-debug-androidTest.apk + # Sign app-debug-androidTest.apk + - name: Sign app-debug-androidTest.apk uses: r0adkll/sign-android-release@v1 with: - releaseDirectory: samples/auth/build/outputs/apk/androidTest/debug + releaseDirectory: samples/app/build/outputs/apk/androidTest/debug signingKeyBase64: ${{ secrets.SIGNING_KEYSTORE }} alias: ${{ secrets.SIGNING_ALIAS }} keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} @@ -58,11 +58,11 @@ jobs: env: BUILD_TOOLS_VERSION: "34.0.0" - # Sign forgerock-auth-debug-androidTest.apk - - name: Sign forgerock-auth-debug-androidTest.apk + # Sign forgerock-integration-tests-debug-androidTest.apk + - name: Sign forgerock-integration-tests-debug-androidTest.apk uses: r0adkll/sign-android-release@v1 with: - releaseDirectory: forgerock-auth/build/outputs/apk/androidTest/debug + releaseDirectory: forgerock-integration-tests/build/outputs/apk/androidTest/debug signingKeyBase64: ${{ secrets.SIGNING_KEYSTORE }} alias: ${{ secrets.SIGNING_ALIAS }} keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} @@ -71,19 +71,19 @@ jobs: BUILD_TOOLS_VERSION: "34.0.0" # Publish the signed APKs as build artifacts - - name: Publish auth-debug-androidTest.apk + - name: Publish app-debug-androidTest.apk uses: actions/upload-artifact@v3 if: success() with: - name: auth-debug-androidTest-signed.apk - path: samples/auth/build/outputs/apk/androidTest/debug/auth-debug-androidTest-signed.apk + name: app-debug-androidTest-signed.apk + path: samples/app/build/outputs/apk/androidTest/debug/app-debug-androidTest-signed.apk - - name: Publish forgerock-auth-debug-androidTest.apk + - name: Publish forgerock-integration-tests-debug-androidTest-signed.apk uses: actions/upload-artifact@v3 if: success() with: - name: forgerock-auth-debug-androidTest-signed.apk - path: forgerock-auth/build/outputs/apk/androidTest/debug/forgerock-auth-debug-androidTest-signed.apk + name: forgerock-integration-tests-debug-androidTest-signed.apk + path: forgerock-integration-tests/build/outputs/apk/androidTest/debug/forgerock-integration-tests-debug-androidTest-signed.apk # Send slack notification ONLY if any of the steps above fail - name: Send slack notification diff --git a/.github/workflows/bitbar-run.yaml b/.github/workflows/bitbar-run.yaml index 4bf72189..6113096e 100644 --- a/.github/workflows/bitbar-run.yaml +++ b/.github/workflows/bitbar-run.yaml @@ -40,28 +40,28 @@ jobs: steps: # Get the test artifacts prepared in previous step - - name: Get the auth-debug-androidTest.apk BitBar artifact + - name: Get the app-debug-androidTest.apk BitBar artifact uses: actions/download-artifact@v3 with: - name: auth-debug-androidTest-signed.apk + name: app-debug-androidTest-signed.apk - - name: Get the forgerock-auth-debug-androidTest.apk BitBar artifact + - name: Get the forgerock-integration-tests-debug-androidTest.apk BitBar artifact uses: actions/download-artifact@v3 with: - name: forgerock-auth-debug-androidTest-signed.apk + name: forgerock-integration-tests-debug-androidTest-signed.apk - - name: Unzip auth-debug-androidTest-signed.apk and forgerock-auth-debug-androidTest-signed.apk + - name: Unzip app-debug-androidTest-signed.apk and forgerock-integration-tests-debug-androidTest-signed.apk run: | - unzip -o auth-debug-androidTest-signed.apk - unzip -o forgerock-auth-debug-androidTest-signed.apk + unzip -o app-debug-androidTest-signed.apk + unzip -o forgerock-integration-tests-debug-androidTest-signed.apk - - name: Upload auth-debug-androidTest-signed.apk to BitBar + - name: Upload app-debug-androidTest-signed.apk to BitBar run: | - echo "BITBAR_APP_FILE_ID=$(curl -X POST -u ${{ secrets.BITBAR_API_KEY }}: https://cloud.bitbar.com/api/me/files -F "file=@auth-debug-androidTest-signed.apk" | jq '.id')" >> $GITHUB_ENV + echo "BITBAR_APP_FILE_ID=$(curl -X POST -u ${{ secrets.BITBAR_API_KEY }}: https://cloud.bitbar.com/api/me/files -F "file=@app-debug-androidTest-signed.apk" | jq '.id')" >> $GITHUB_ENV - - name: Upload forgerock-auth-debug-androidTest-signed.apk to BitBar + - name: Upload forgerock-integration-tests-debug-androidTest-signed.apk to BitBar run: | - echo "BITBAR_TEST_FILE_ID=$(curl -X POST -u ${{ secrets.BITBAR_API_KEY }}: https://cloud.bitbar.com/api/me/files -F "file=@forgerock-auth-debug-androidTest-signed.apk" | jq '.id')" >> $GITHUB_ENV + echo "BITBAR_TEST_FILE_ID=$(curl -X POST -u ${{ secrets.BITBAR_API_KEY }}: https://cloud.bitbar.com/api/me/files -F "file=@forgerock-integration-tests-debug-androidTest-signed.apk" | jq '.id')" >> $GITHUB_ENV - name: Prepare BitBar run configuration file run: | diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index bc325bd1..e5f000e2 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -43,13 +43,17 @@ jobs: - name: Run forgerock-authenticator debug unit tests run: ./gradlew :forgerock-authenticator:testDebugUnitTest --stacktrace --no-daemon + # Execute forgerock-authenticator debug unit tests + - name: Run ping-protect debug unit tests + run: ./gradlew :ping-protect:testDebugUnitTest --stacktrace --no-daemon + # Publish test reports for the unit tests - name: Publish test results if: success() || failure() uses: dorny/test-reporter@v1 with: name: Unit tests results - path: 'forgerock-core/build/test-results/**/TEST-*.xml,forgerock-auth/build/test-results/**/TEST-*.xml,forgerock-authenticator/build/test-results/**/TEST-*.xml' + path: 'forgerock-core/build/test-results/**/TEST-*.xml,forgerock-auth/build/test-results/**/TEST-*.xml,forgerock-authenticator/build/test-results/**/TEST-*.xml,ping-protect/build/test-results/**/TEST-*.xml' list-suites: 'all' list-tests: 'all' fail-on-error: 'true' diff --git a/.github/workflows/run-live-tests.yaml b/.github/workflows/run-live-tests.yaml index 4d0f58a9..2236a08b 100644 --- a/.github/workflows/run-live-tests.yaml +++ b/.github/workflows/run-live-tests.yaml @@ -6,7 +6,7 @@ on: description: The AM url to run live test cases against type: string required: true - default: https://openam-forgerrock-sdksteanant.forgeblocks.com/am + default: https://openam-sdks.forgeblocks.com/am realm: description: The AM realm to use type: string @@ -16,7 +16,7 @@ on: description: The AM session cookie name type: string required: true - default: iPlanetDirectoryPro + default: 5421aeddf91aa20 jobs: run-tests: diff --git a/CHANGELOG.md b/CHANGELOG.md index d37fb917..5df6b084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [4.4.0] +#### Added +- Added `TextInput` callback support [SDKS-545] +- Added a new module for integration with `PingOne Protect` [SDKS-2900] +- Added interface allowing developers to customize the biometric prompt for device binding\signing [SDKS-2991] +- Added immutable HTTP headers on each request `x-requested-with: forgerock-sdk` and `x-requested-platform: android` [SDKS-3033] + +#### Fixed +- Addressed `nimbus-jose-jwt:9.25` library security vulnerability (CVE-2023-52428) [SDKS-2988] +- NullPointerException for Centralize Login, Replace deprecated onActivityResult with ActivityResultContract [SDKS-3079] + ## [4.3.1] #### Fixed - Fixed an issue where the SDK was crashing during device binding on Android 9 devices [SDKS-2948] diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 17517f38..00000000 --- a/build.gradle +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (c) 2019 - 2023 ForgeRock. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - ext { - kotlin_version = '1.8.0' - customCSSFile = projectDir.toString() + "/dokka/fr-backstage-styles.css" - customLogoFile = projectDir.toString() + "/dokka/logo-icon.svg" - customTemplatesFolder = file(projectDir.toString() + "/dokka/templates") - } - repositories { - google() - mavenCentral() - maven { - url "https://plugins.gradle.org/m2/" - } - } - dependencies { - classpath 'com.android.tools.build:gradle:8.1.0' - classpath "com.adarshr:gradle-test-logger-plugin:2.0.0" - classpath 'com.google.gms:google-services:4.3.15' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -plugins { - id('io.github.gradle-nexus.publish-plugin') version '1.1.0' - id('org.sonatype.gradle.plugins.scan') version '2.4.0' - id("org.jetbrains.dokka") version "1.9.10" -} - - -allprojects { - configurations.all { - - resolutionStrategy { - // Due to vulnerability [CVE-2022-40152] from dokka project. - force 'com.fasterxml.jackson.module:jackson-module-kotlin:2.13.5' - force 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.13.5' - force 'com.fasterxml.jackson.core:jackson-databind:2.13.5' - // Junit test project - force 'junit:junit:4.13.2' - //Due to Vulnerability [CVE-2022-2390]: CWE-471 The product does not properly - // protect an assumed-immutable element from being modified by an attacker. - // on version < 18.0.1, this library is depended by most of the google libraries. - // and needs to be reviewed on upgrades - force 'com.google.android.gms:play-services-basement:18.1.0' - //Due to Vulnerability [CVE-2023-3635] CWE-681: Incorrect Conversion between Numeric Types - //on version < 3.4.0, this library is depended by okhttp, when okhttp upgrade, this needs - //to be reviewed - force 'com.squareup.okio:okio:3.4.0' - //Due to this https://github.com/powermock/powermock/issues/1125, we have to keep using an - //older version of mockito until mockito release a fix - force 'org.mockito:mockito-core:3.12.4' - // this is for the mockwebserver - force 'org.bouncycastle:bcprov-jdk15on:1.68' - } - } - repositories { - google() - mavenCentral() - } - -} - -subprojects { - apply plugin: "org.jetbrains.dokka" - - tasks.named("dokkaHtml") { - pluginsMapConfiguration.set( - [ - "org.jetbrains.dokka.base.DokkaBase": """{ - "customStyleSheets": ["$customCSSFile"], - "templatesDir": "$customTemplatesFolder" - }""" - ] - ) - } - - tasks.named("dokkaHtmlPartial") { - pluginsMapConfiguration.set( - [ - "org.jetbrains.dokka.base.DokkaBase": """{ - "customStyleSheets": ["$customCSSFile"], - "templatesDir": "$customTemplatesFolder" - }""" - ] - ) - } - - //Powermock compatibility with jdk 17 - tasks.withType(Test).configureEach{ - jvmArgs = jvmArgs + ['--add-opens=java.base/java.lang=ALL-UNNAMED'] - jvmArgs = jvmArgs + ['--add-opens=java.base/java.security=ALL-UNNAMED'] - jvmArgs = jvmArgs + ['--add-opens=java.base/java.security.cert=ALL-UNNAMED'] - } - -} - -afterEvaluate { - tasks.named("dokkaHtmlMultiModule") { - moduleName.set("ForgeRock SDK for Android") - moduleVersion.set(project.property('VERSION')) - outputDirectory.set(file("build/api-reference/html")) - pluginsMapConfiguration.set( - [ - "org.jetbrains.dokka.base.DokkaBase": """{ - "customStyleSheets": ["$customCSSFile"], - "customAssets": ["$customLogoFile"], - "templatesDir": "$customTemplatesFolder" - }""" - ] - ) - } - tasks.named("dokkaJavadocCollector") { - moduleName.set("ForgeRock SDK for Android Javadoc") - moduleVersion.set(project.property('VERSION')) - outputDirectory.set(file("build/api-reference/javadoc")) - } - -} - - -ossIndexAudit { - username = System.properties['username'] - password = System.properties['password'] - excludeVulnerabilityIds = ['CVE-2020-15250'] -} - -task clean(type: Delete) { - delete rootProject.buildDir -} - -project.ext.versionName = VERSION -project.ext.versionCode = VERSION_CODE - -ext["signing.keyId"] = '' -ext["signing.password"] = '' -ext["signing.secretKeyRingFile"] = '' -ext["ossrhUsername"] = '' -ext["ossrhPassword"] = '' - -File secretPropsFile = project.rootProject.file('local.properties') -if (secretPropsFile.exists()) { - Properties p = new Properties() - p.load(new FileInputStream(secretPropsFile)) - p.each { name, value -> - ext[name] = value - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..25b0e0fa --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import org.jetbrains.dokka.DokkaConfiguration.Visibility +import org.jetbrains.dokka.gradle.DokkaTask +import java.io.FileInputStream +import java.util.Properties + +val customCSSFile = "$projectDir/dokka/fr-backstage-styles.css" +val customLogoFile = "$projectDir/dokka/logo-icon.svg" +val customTemplatesFolder = file("$projectDir/dokka/templates") + +buildscript { + + dependencies { + classpath("com.android.tools.build:gradle:8.2.2") + classpath("com.adarshr:gradle-test-logger-plugin:2.0.0") + classpath("com.google.gms:google-services:4.3.15") + } +} + +plugins { + id("io.github.gradle-nexus.publish-plugin") version "1.1.0" + id("org.sonatype.gradle.plugins.scan") version "2.4.0" + id("org.jetbrains.dokka") version "1.9.10" + id("com.android.application") version "8.2.2" apply false + id("com.android.library") version "8.2.2" apply false + id("org.jetbrains.kotlin.android") version "1.9.22" apply false +} + +// Configure all single-project Dokka tasks at the same time, +// such as dokkaHtml, dokkaJavadoc and dokkaGfm. +// Configure all single-project Dokka tasks at the same time, +// such as dokkaHtml, dokkaJavadoc and dokkaGfm. +tasks.withType().configureEach { + dokkaSourceSets.configureEach { + sourceRoots.setFrom(file("$buildDir/src-delomboked")) + } +} + +allprojects { + configurations.all { + + resolutionStrategy { + // Due to vulnerability [CVE-2022-40152] from dokka project. + force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.5") + force("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.13.5") + force("com.fasterxml.jackson.core:jackson-databind:2.13.5") + // Junit test project + force("junit:junit:4.13.2") + //Due to Vulnerability [CVE-2022-2390]: CWE-471 The product does not properly + // protect an assumed-immutable element from being modified by an attacker. + // on version < 18.0.1, this library is depended by most of the google libraries. + // and needs to be reviewed on upgrades + force("com.google.android.gms:play-services-basement:18.1.0") + //Due to Vulnerability [CVE-2023-3635] CWE-681: Incorrect Conversion between Numeric Types + //on version < 3.4.0, this library is depended by okhttp, when okhttp upgrade, this needs + //to be reviewed + force("com.squareup.okio:okio:3.4.0") + //Due to this https://github.com/powermock/powermock/issues/1125, we have to keep using an + //older version of mockito until mockito release a fix + force("org.mockito:mockito-core:3.12.4") + // this is for the mockwebserver + force("org.bouncycastle:bcprov-jdk15on:1.68") + } + } +} + +subprojects { + + apply(plugin = "org.jetbrains.dokka") + + tasks.dokkaHtml { + val map = mutableMapOf() + map["org.jetbrains.dokka.base.DokkaBase"] = """{ + "customStyleSheets": ["$customCSSFile"], + "templatesDir": "$customTemplatesFolder" + }""" + pluginsMapConfiguration.set(map) + moduleVersion.set(project.property("VERSION") as? String) + outputDirectory.set(file("build/html/${project.name}-dokka")) + + dokkaSourceSets.configureEach { + documentedVisibilities.set( + setOf( + Visibility.PUBLIC, + Visibility.PROTECTED, + Visibility.PRIVATE, + Visibility.INTERNAL, + Visibility.PACKAGE + ) + ) + perPackageOption { + matchingRegex.set(".*internal.*") + suppress.set(true) + } + } + } + + + tasks.withType().configureEach { + jvmArgs = jvmArgs?.plus("--add-opens=java.base/java.lang=ALL-UNNAMED") + jvmArgs = jvmArgs?.plus("--add-opens=java.base/java.security=ALL-UNNAMED") + jvmArgs = jvmArgs?.plus("--add-opens=java.base/java.security.cert=ALL-UNNAMED") + } + +} + +afterEvaluate { + + tasks.dokkaHtmlMultiModule { + moduleName.set("ForgeRock SDK for Android") + moduleVersion.set(project.property("VERSION") as? String) + outputDirectory.set(file("build/api-reference/html")) + val map = mutableMapOf() + map["org.jetbrains.dokka.base.DokkaBase"] = """{ + "customStyleSheets": ["$customCSSFile"], + "templatesDir": "$customTemplatesFolder" + }""" + pluginsMapConfiguration.set(map) + } + + + tasks.dokkaJavadocCollector { + moduleName.set("ForgeRock SDK for Android Javadoc") + moduleVersion.set(project.property("VERSION") as? String) + outputDirectory.set(file("build/api-reference/javadoc")) + } + +} + + +ossIndexAudit { + username = System.getProperty("username") + password = System.getProperty("password") + excludeVulnerabilityIds = setOf("CVE-2020-15250") +} + +tasks.register("clean").configure { + delete(rootProject.buildDir) +} + +// Need to be removed this in future +project.ext.set("versionName", project.property("VERSION") as? String ?: "") +project.ext.set("versionCode", project.property("VERSION_CODE") as? Int ?: 1) +project.ext["signing.keyId"] = "" +project.ext["signing.password"] = "" +project.ext["signing.secretKeyRingFile"] = "" +project.ext["ossrhUsername"] = "" +project.ext["ossrhPassword"] = "" + +val secretPropsFile: File = project.rootProject.file("local.properties") +if (secretPropsFile.exists()) { + val p = Properties() + p.load(FileInputStream(secretPropsFile)) + p.forEach { name, value -> + ext[(name as? String).toString()] = value + } +} \ No newline at end of file diff --git a/buildSrc/.gitignore b/buildSrc/.gitignore new file mode 100644 index 00000000..f06dfad6 --- /dev/null +++ b/buildSrc/.gitignore @@ -0,0 +1,2 @@ +.gradle +build \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..0068e591 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +plugins { + `kotlin-dsl` +} + +repositories { + google() + mavenCentral() +} + +dependencies { + implementation("com.android.tools.build:gradle-api:8.2.2") +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/AndroidBuildGradlePlugin.kt b/buildSrc/src/main/kotlin/AndroidBuildGradlePlugin.kt new file mode 100644 index 00000000..25625bad --- /dev/null +++ b/buildSrc/src/main/kotlin/AndroidBuildGradlePlugin.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import com.android.build.api.dsl.LibraryExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project + +class AndroidBuildGradlePlugin : Plugin { + + override fun apply(project: Project) { + project.android().apply { + compileSdk = 34; + defaultConfig { + minSdk = 23 + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), + ("proguard-rules.pro")) + } + } + testOptions { + targetSdk = 34 + unitTests { + isIncludeAndroidResources = true + isReturnDefaultValues = true + } + unitTests.all { + it.exclude("**/*TestSuite*") + } + } + + buildFeatures { + buildConfig = true + } + + useLibrary("android.test.base") + useLibrary("android.test.mock") + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + } + } + + /** + * Extension function. + */ + private fun Project.android(): LibraryExtension { + return extensions.getByType(LibraryExtension::class.java) + } + + +} \ No newline at end of file diff --git a/config/kdoc.gradle b/config/kdoc.gradle index 04f34b2e..10debeb8 100644 --- a/config/kdoc.gradle +++ b/config/kdoc.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 ForgeRock. All rights reserved. + * Copyright (c) 2023-2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -11,9 +11,6 @@ apply plugin: 'org.jetbrains.dokka' /** * Generate Kdoc, delombok then generate Javadoc */ -configurations { - delombok -} task delombok { def srcJava = 'src/main/java' @@ -23,6 +20,10 @@ task delombok { classname: 'lombok.delombok.ant.Tasks$Delombok', classpath: configurations.delombok.asPath) ant.delombok(verbose: 'true', from: srcJava, to: "$buildDir/src-delomboked") +// copy { +// from "$buildDir/src-delomboked" +// into "src/main/java" +// } } } @@ -36,25 +37,6 @@ tasks.named("dokkaJavadoc").configure { dependsOn("bundleLibCompileToJarRelease") } -dokkaJavadoc { - dokkaSourceSets { - named("main") { - displayName.set(name) - outputDirectory = file("build/javadoc/$project.name-dokka") - sourceDirs = files("$buildDir/src-delomboked") - sourceRoots.setFrom(file("$buildDir/src-delomboked")) - reportUndocumented.set(false) - skipEmptyPackages.set(true) - skipDeprecated.set(false) - suppressGeneratedFiles.set(true) - noStdlibLink.set(false) - noJdkLink.set(false) - noAndroidSdkLink.set(false) - suppress.set(false) - } - } -} - tasks.named("dokkaHtml").configure { dependsOn("generateDebugRFile") dependsOn("bundleLibCompileToJarDebug") @@ -62,22 +44,6 @@ tasks.named("dokkaHtml").configure { dependsOn("bundleLibCompileToJarRelease") } -dokkaHtml { - dokkaSourceSets { - named("main") { - outputDirectory = file("build/html/$project.name-dokka") - sourceDirs = files("$buildDir/src-delomboked") - sourceRoots.setFrom(file("$buildDir/src-delomboked")) - noAndroidSdkLink.set(false) - includeNonPublic.set(true) - skipEmptyPackages.set(true) - reportUndocumented.set(true) - skipDeprecated.set(false) - - } - } -} - task sourcesJar(type: Jar) { archiveClassifier.set("sources") from android.sourceSets.main.java.srcDirs diff --git a/forgerock-auth-ui/build.gradle b/forgerock-auth-ui/build.gradle index 7db87fc5..6860fd6b 100644 --- a/forgerock-auth-ui/build.gradle +++ b/forgerock-auth-ui/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -36,14 +36,11 @@ android { buildFeatures { viewBinding true + buildConfig true } } -apply from: '../config/kdoc.gradle' -apply from: '../config/publish.gradle' - - dependencies { api project(':forgerock-auth') @@ -61,8 +58,4 @@ dependencies { compileOnly 'com.google.android.gms:play-services-fido:20.0.1' - compileOnly "org.projectlombok:lombok:1.18.28" - delombok "org.projectlombok:lombok:1.18.28" - annotationProcessor 'org.projectlombok:lombok:1.18.28' - } diff --git a/forgerock-auth-ui/src/androidTest/java/org/forgerock/android/auth/ui/ExampleInstrumentedTest.java b/forgerock-auth-ui/src/androidTest/java/org/forgerock/android/auth/ui/ExampleInstrumentedTest.java deleted file mode 100644 index ade7ef71..00000000 --- a/forgerock-auth-ui/src/androidTest/java/org/forgerock/android/auth/ui/ExampleInstrumentedTest.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2019 ForgeRock. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -package org.forgerock.android.auth.ui; - -import android.content.Context; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("org.forgerock.android.auth.ui.test", appContext.getPackageName()); - } -} diff --git a/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/AdviceDialogFragment.java b/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/AdviceDialogFragment.java index fc1bb034..ae99866d 100644 --- a/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/AdviceDialogFragment.java +++ b/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/AdviceDialogFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -29,7 +29,6 @@ import kotlin.Result; import kotlin.Unit; import kotlin.coroutines.Continuation; -import lombok.Setter; /** * Reference implementation of handing Advice with {@link DialogFragment} @@ -41,7 +40,10 @@ public class AdviceDialogFragment extends DialogFragment implements AuthHandler private FRViewModel viewModel; private boolean isCancel = true; - @Setter + public void setListener(Continuation listener) { + this.listener = listener; + } + private Continuation listener; private PolicyAdvice advice; diff --git a/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/CallbackFragmentFactory.java b/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/CallbackFragmentFactory.java index b204c5d3..0a81a22d 100644 --- a/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/CallbackFragmentFactory.java +++ b/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/CallbackFragmentFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -66,6 +66,7 @@ private CallbackFragmentFactory() { register(WebAuthnAuthenticationCallback.class, WebAuthnAuthenticationCallbackFragment.class); register(SelectIdPCallback.class, SelectIdPCallbackFragment.class); register(IdPCallback.class, IdPCallbackFragment.class); + register(TextInputCallback.class, TextInputCallbackFragment.class); } public static CallbackFragmentFactory getInstance() { diff --git a/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/FRViewModel.java b/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/FRViewModel.java index 31ee7b1d..f5c18594 100644 --- a/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/FRViewModel.java +++ b/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/FRViewModel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 - 2020 ForgeRock. All rights reserved. + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -18,18 +18,26 @@ import org.forgerock.android.auth.NodeListener; import org.forgerock.android.auth.PolicyAdvice; -import lombok.Getter; - /** * {@link ViewModel} Wrapper for {@link FRUser} */ public abstract class FRViewModel extends ViewModel { - @Getter private MutableLiveData> nodeLiveData = new MutableLiveData<>(); - @Getter private MutableLiveData resultLiveData = new MutableLiveData<>(); - @Getter + + public MutableLiveData> getNodeLiveData() { + return nodeLiveData; + } + + public MutableLiveData getResultLiveData() { + return resultLiveData; + } + + public MutableLiveData> getExceptionLiveData() { + return exceptionLiveData; + } + private MutableLiveData> exceptionLiveData = new MutableLiveData<>(); private NodeListener nodeListener; diff --git a/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/SingleLiveEvent.java b/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/SingleLiveEvent.java index 773a9157..79bac3c3 100644 --- a/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/SingleLiveEvent.java +++ b/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/SingleLiveEvent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 ForgeRock. All rights reserved. + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -7,16 +7,18 @@ package org.forgerock.android.auth.ui; -import lombok.AllArgsConstructor; /** * Event only trigger once after configuration change. */ -@AllArgsConstructor public class SingleLiveEvent { private T value; + public SingleLiveEvent(T value) { + this.value = value; + } + public T getValue() { T result = value; value = null; diff --git a/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/callback/TextInputCallbackFragment.java b/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/callback/TextInputCallbackFragment.java new file mode 100644 index 00000000..020bb301 --- /dev/null +++ b/forgerock-auth-ui/src/main/java/org/forgerock/android/auth/ui/callback/TextInputCallbackFragment.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package org.forgerock.android.auth.ui.callback; + +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import com.google.android.material.textfield.TextInputLayout; + +import org.forgerock.android.auth.callback.TextInputCallback; +import org.forgerock.android.auth.ui.R; + +/** + * UI representation for {@link TextInputCallback} + */ +public class TextInputCallbackFragment extends CallbackFragment { + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + final View view = inflater.inflate(R.layout.fragment_text_input_callback, container, false); + + EditText text = view.findViewById(R.id.text); + if (callback.getDefaultText() != null) { + text.setText(callback.getDefaultText().toString()); + } + TextInputLayout textInputLayout = view.findViewById(R.id.textInputLayout); + textInputLayout.setHint(callback.getPrompt()); + text.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + callback.setValue(s.toString()); + onDataCollected(); + } + }); + return view; + } +} diff --git a/forgerock-auth-ui/src/main/res/layout/fragment_text_input_callback.xml b/forgerock-auth-ui/src/main/res/layout/fragment_text_input_callback.xml new file mode 100644 index 00000000..0bcdb575 --- /dev/null +++ b/forgerock-auth-ui/src/main/res/layout/fragment_text_input_callback.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/forgerock-auth/build.gradle b/forgerock-auth/build.gradle deleted file mode 100644 index d3753039..00000000 --- a/forgerock-auth/build.gradle +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (c) 2019 - 2023 ForgeRock. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -apply plugin: 'com.android.library' -apply plugin: "com.adarshr.test-logger" -apply plugin: 'maven-publish' -apply plugin: 'signing' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-parcelize' - -android { - namespace 'org.forgerock.android.auth' - testNamespace 'org.forgerock.android.auth.androidTest' - - compileSdk 34 - useLibrary 'android.test.base' - useLibrary 'android.test.mock' - - defaultConfig { - minSdkVersion 23 - targetSdkVersion 34 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - manifestPlaceholders = [ - 'appAuthRedirectScheme': 'org.forgerock.demo' - ] - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - - debug { - } - } - - /* Comment this to debug instrument Test, - * There is an issue for AS to run NDK with instrument Test */ - externalNativeBuild { - ndkBuild { - path 'src/main/jni/Android.mk' - } - } - - testOptions { - unitTests.includeAndroidResources = true - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - packagingOptions { - jniLibs { - pickFirsts += ['**/*.so'] - } - } - testOptions { - unitTests.all { - exclude '**/*TestSuite*' - } - } - buildFeatures { - viewBinding true - } - - kotlinOptions { - freeCompilerArgs = ['-Xjvm-default=all'] - } - -} - -apply from: '../config/logger.gradle' -apply from: '../config/kdoc.gradle' -apply from: '../config/publish.gradle' -/** - * Dependencies - * - */ -dependencies { - api project(':forgerock-core') - implementation fileTree(dir: 'libs', include: ['*.jar']) - - implementation 'com.squareup.okhttp3:okhttp:4.11.0' - implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0' - implementation 'androidx.annotation:annotation:1.6.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - - def coroutine_version = '1.7.2' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutine_version" - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.fragment:fragment-ktx:1.6.1' - - //Make it optional for developer - compileOnly 'com.google.android.gms:play-services-location:21.0.1' - compileOnly 'com.google.android.gms:play-services-safetynet:18.0.1' - // Keeping this version for now, its breaking Apple SignIn for the later versions. - compileOnly 'net.openid:appauth:0.11.1' - compileOnly 'com.google.android.gms:play-services-fido:20.0.1' - - //For Device Binding - compileOnly 'androidx.biometric:biometric-ktx:1.2.0-alpha05' - compileOnly 'com.nimbusds:nimbus-jose-jwt:9.25' - - //Application Pin - compileOnly 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0' - - //Social Login - compileOnly 'com.google.android.gms:play-services-auth:20.6.0' - compileOnly 'com.facebook.android:facebook-login:16.0.0' - - //For App integrity - compileOnly 'com.google.android.play:integrity:1.3.0' - - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation 'com.squareup.okhttp:mockwebserver:2.7.5' - androidTestImplementation 'commons-io:commons-io:2.6' - androidTestImplementation 'androidx.test:rules:1.5.0' - androidTestImplementation 'com.google.android.gms:play-services-location:21.0.1' - //Do not update to the latest library, Only 2.x compatible with Android M and below. - androidTestImplementation 'org.assertj:assertj-core:2.9.1' - androidTestImplementation 'com.google.android.gms:play-services-fido:20.0.1' - - androidTestImplementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05' - androidTestImplementation 'com.nimbusds:nimbus-jose-jwt:9.25' - //For Application Pin - androidTestImplementation 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0' - androidTestImplementation 'androidx.security:security-crypto:1.1.0-alpha06' - - //App Integrity - androidTestImplementation 'com.google.android.play:integrity:1.3.0' - - testImplementation 'androidx.test:core:1.5.0' - testImplementation 'androidx.test.ext:junit:1.1.5' - testImplementation 'androidx.test:runner:1.5.2' - testImplementation 'androidx.fragment:fragment-testing:1.6.1' - testImplementation 'com.nimbusds:nimbus-jose-jwt:9.25' - - testImplementation 'junit:junit:4.13.2' - testImplementation 'org.robolectric:robolectric:4.9.2' - testImplementation 'com.squareup.okhttp:mockwebserver:2.7.5' - testImplementation 'commons-io:commons-io:2.6' - testImplementation 'org.assertj:assertj-core:3.23.1' - testImplementation "androidx.test.espresso:espresso-intents:3.5.1" - testImplementation 'net.openid:appauth:0.11.1' - testImplementation 'com.google.android.gms:play-services-fido:20.0.1' - testImplementation 'com.google.android.gms:play-services-auth:20.6.0' - testImplementation 'com.facebook.android:facebook-login:16.0.0' - testImplementation 'com.google.android.gms:play-services-safetynet:18.0.1' - testImplementation 'org.jeasy:easy-random-core:4.0.0' - testImplementation 'com.nimbusds:nimbus-jose-jwt:9.25' - testImplementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05' - - //Application Pin - testImplementation 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0' - testImplementation 'androidx.security:security-crypto:1.1.0-alpha06' - - //App Integrity - testImplementation 'com.google.android.play:integrity:1.3.0' - - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2' - - testImplementation 'org.mockito:mockito-core:4.8.1' - testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0' - testImplementation 'org.powermock:powermock-module-junit4:2.0.9' - testImplementation 'org.powermock:powermock-api-mockito2:2.0.9' - - compileOnly "org.projectlombok:lombok:1.18.28" - delombok "org.projectlombok:lombok:1.18.28" - annotationProcessor 'org.projectlombok:lombok:1.18.28' - -} -repositories { - mavenCentral() -} \ No newline at end of file diff --git a/forgerock-auth/build.gradle.kts b/forgerock-auth/build.gradle.kts new file mode 100644 index 00000000..590c9bad --- /dev/null +++ b/forgerock-auth/build.gradle.kts @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +plugins { + + alias(libs.plugins.androidLibrary) + alias(libs.plugins.kotlinAndroid) + + + id("com.adarshr.test-logger") + id("maven-publish") + id("signing") + id("kotlin-parcelize") +} + +apply() + +android { + namespace = "org.forgerock.android.auth" + testNamespace = "org.forgerock.android.auth.androidTest" + + buildFeatures { + //Do we really need this? + viewBinding = true + } + + kotlinOptions { + freeCompilerArgs = listOf("-Xjvm-default=all") + } + + unitTestVariants.all { + this.mergedFlavor.manifestPlaceholders["appAuthRedirectScheme"] = "org.forgerock.demo" + } + +} + +apply("../config/logger.gradle") +apply("../config/kdoc.gradle") +apply("../config/publish.gradle") +/** + * Dependencies + */ + +val delombok by configurations.creating { + extendsFrom(configurations.compileOnly.get()) +} + +dependencies { + api(project(":forgerock-core")) + + implementation(libs.okhttp) + implementation(libs.logging.interceptor) + implementation(libs.androidx.annotation) + implementation(libs.androidx.constraintlayout) + + implementation(libs.org.jetbrains.kotlinx) + implementation(libs.jetbrains.kotlinx.coroutines.play.services) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.fragment.ktx) + + //Make it optional for developer + compileOnly(libs.play.services.location) + compileOnly(libs.play.services.safetynet) + // Keeping this version for now, its breaking Apple SignIn for the later versions. + compileOnly(libs.appauth) + compileOnly(libs.play.services.fido) + + //For Device Binding + compileOnly(libs.androidx.biometric.ktx) + compileOnly(libs.nimbus.jose.jwt) + + //Application Pin + compileOnly(libs.bcpkix.jdk15on) + + //Social Login + compileOnly(libs.play.services.auth) + compileOnly(libs.facebook.login) + + //For App integrity + compileOnly(libs.integrity) + + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.espresso.core) + androidTestImplementation(libs.mockwebserver) + androidTestImplementation(libs.commons.io) + androidTestImplementation(libs.rules) + androidTestImplementation(libs.play.services.location) + //Do not update to the latest library, Only 2.x compatible with Android M and below. + androidTestImplementation(libs.assertj.core) + androidTestImplementation(libs.play.services.fido) + + androidTestImplementation(libs.androidx.biometric.ktx) + androidTestImplementation(libs.nimbus.jose.jwt) + //For Application Pin + androidTestImplementation(libs.bcpkix.jdk15on) + androidTestImplementation(libs.androidx.security.crypto) + + //App Integrity + androidTestImplementation(libs.integrity) + + testImplementation(libs.androidx.test.core) + testImplementation(libs.androidx.test.ext.junit) + testImplementation(libs.androidx.test.runner) + testImplementation(libs.androidx.fragment.testing) + testImplementation(libs.nimbus.jose.jwt) + + testImplementation(libs.junit) + testImplementation(libs.org.robolectric.robolectric) + testImplementation(libs.mockwebserver) + testImplementation(libs.commons.io) + testImplementation(libs.assertj.core) + testImplementation(libs.androidx.espresso.intents) + testImplementation(libs.appauth) + testImplementation(libs.play.services.fido) + testImplementation(libs.play.services.auth) + testImplementation(libs.facebook.login) + testImplementation(libs.play.services.safetynet) + testImplementation(libs.easy.random.core) + testImplementation(libs.nimbus.jose.jwt) + testImplementation(libs.androidx.biometric.ktx) + + //Application Pin + testImplementation(libs.bcpkix.jdk15on) + testImplementation(libs.androidx.security.crypto) + + //App Integrity + testImplementation(libs.integrity) + + testImplementation(libs.kotlinx.coroutines.test) + + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.powermock.module.junit4) + testImplementation(libs.powermock.api.mockito2) + + compileOnly(libs.projectlombok.lombok) + delombok(libs.projectlombok.lombok) + annotationProcessor(libs.projectlombok.lombok) + +} \ No newline at end of file diff --git a/forgerock-auth/proguard-rules.pro b/forgerock-auth/proguard-rules.pro index f1b42451..2f9dc5a4 100644 --- a/forgerock-auth/proguard-rules.pro +++ b/forgerock-auth/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/AndroidBaseTest.java b/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/AndroidBaseTest.java deleted file mode 100644 index 008fa8c5..00000000 --- a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/AndroidBaseTest.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2020 ForgeRock. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -package org.forgerock.android.auth; - -import android.content.Context; - -import androidx.test.core.app.ApplicationProvider; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.rules.Timeout; - -import java.util.concurrent.TimeUnit; - -public abstract class AndroidBaseTest { - - @Rule - public Timeout timeout = new Timeout(10000, TimeUnit.MILLISECONDS); - protected Context context = ApplicationProvider.getApplicationContext(); - public static String USERNAME = "sdkuser"; - public static String PASSWORD = "password"; - public static String USER_EMAIL = "sdkuser@example.com"; - - protected String TREE = "UsernamePassword"; - - @Before - public void setUpSDK() { - Logger.set(Logger.Level.DEBUG); - FRAuth.start(context); - } -} diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/AppAuthConfigurer.java b/forgerock-auth/src/main/java/org/forgerock/android/auth/AppAuthConfigurer.java index 183577d5..dbf30085 100644 --- a/forgerock-auth/src/main/java/org/forgerock/android/auth/AppAuthConfigurer.java +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/AppAuthConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 ForgeRock. All rights reserved. + * Copyright (c) 2020-2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -20,8 +20,6 @@ import java.net.MalformedURLException; -import lombok.AccessLevel; -import lombok.Getter; import lombok.RequiredArgsConstructor; /** @@ -33,16 +31,12 @@ public class AppAuthConfigurer { private final FRUser.Browser parent; - @Getter(AccessLevel.PACKAGE) private Consumer authorizationRequestBuilder = builder -> { }; - @Getter(AccessLevel.PACKAGE) private androidx.core.util.Consumer appAuthConfigurationBuilder = builder -> { }; - @Getter(AccessLevel.PACKAGE) private Consumer customTabsIntentBuilder = builder -> { }; - @Getter(AccessLevel.PACKAGE) private Supplier authorizationServiceConfigurationSupplier = () -> { OAuth2Client oAuth2Client = Config.getInstance().getOAuth2Client(); try { @@ -112,4 +106,19 @@ public FRUser.Browser done() { return parent; } + Consumer getAuthorizationRequestBuilder() { + return this.authorizationRequestBuilder; + } + + Consumer getAppAuthConfigurationBuilder() { + return this.appAuthConfigurationBuilder; + } + + Consumer getCustomTabsIntentBuilder() { + return this.customTabsIntentBuilder; + } + + Supplier getAuthorizationServiceConfigurationSupplier() { + return this.authorizationServiceConfigurationSupplier; + } } diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/AppAuthFragment.java b/forgerock-auth/src/main/java/org/forgerock/android/auth/AppAuthFragment.java index 074157a3..42b041ba 100644 --- a/forgerock-auth/src/main/java/org/forgerock/android/auth/AppAuthFragment.java +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/AppAuthFragment.java @@ -32,6 +32,7 @@ /** * Headless Fragment to receive callback result from AppAuth library */ +@Deprecated @RestrictTo(RestrictTo.Scope.LIBRARY) public class AppAuthFragment extends Fragment { @@ -92,7 +93,7 @@ public void onCreate(Bundle savedInstanceState) { startActivityForResult(intent, AUTH_REQUEST_CODE); } catch (ActivityNotFoundException e) { if (browser.isFailedOnNoBrowserFound()) { - throw e; + Listener.onException(browser.getListener(), e); } } } diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/AppAuthFragment2.kt b/forgerock-auth/src/main/java/org/forgerock/android/auth/AppAuthFragment2.kt new file mode 100644 index 00000000..84ad7333 --- /dev/null +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/AppAuthFragment2.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth + +import android.content.Intent +import android.os.Bundle +import androidx.annotation.RestrictTo +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import kotlinx.coroutines.flow.MutableStateFlow +import net.openid.appauth.AuthorizationResponse +import org.forgerock.android.auth.centralize.BrowserLauncher +import org.forgerock.android.auth.centralize.Launcher + +private const val PENDING = "pending" + +/** + * Headless Fragment to receive callback result from AppAuth library + */ +@RestrictTo(RestrictTo.Scope.LIBRARY) +class AppAuthFragment2 : Fragment() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val state: MutableStateFlow = MutableStateFlow(null) + val delegate = + registerForActivityResult(AuthorizeContract()) { + state.value = it + } + + val pending = savedInstanceState?.getBoolean(PENDING, false) ?: false + + BrowserLauncher.init(Launcher(delegate, state, pending)) + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(PENDING, true) + super.onSaveInstanceState(outState) + } + + override fun onDestroy() { + super.onDestroy() + BrowserLauncher.reset() + } + + companion object { + val TAG: String = AppAuthFragment2::class.java.name + + /** + * Initialize the Fragment to receive AppAuth callback event. + */ + @Synchronized + @JvmStatic + fun launch(activity: FragmentActivity, browser: FRUser.Browser) { + val fragmentManager: FragmentManager = activity.supportFragmentManager + var current = fragmentManager.findFragmentByTag(TAG) as? AppAuthFragment2 + if (current == null) { + current = AppAuthFragment2() + fragmentManager.beginTransaction().add(current, TAG).commitNow() + } + + BrowserLauncher.authorize(browser, object : FRListener { + override fun onSuccess(result: AuthorizationResponse) { + reset(activity, current) + browser.listener.onSuccess(result) + } + + override fun onException(e: Exception) { + reset(activity, current) + browser.listener.onException(e) + } + + /** + * Once receive the result, reset state. + */ + private fun reset(activity: FragmentActivity, fragment: Fragment?) { + activity.runOnUiThread { + BrowserLauncher.reset() + } + fragment?.let { + activity.runOnUiThread { + fragmentManager.beginTransaction().remove(it).commitNow() + } + } + } + }) + + + } + } +} diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/AuthorizeContract.kt b/forgerock-auth/src/main/java/org/forgerock/android/auth/AuthorizeContract.kt new file mode 100644 index 00000000..1c4bbed8 --- /dev/null +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/AuthorizeContract.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package org.forgerock.android.auth + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContract +import androidx.browser.customtabs.CustomTabsIntent +import net.openid.appauth.AppAuthConfiguration +import net.openid.appauth.AuthorizationRequest +import net.openid.appauth.AuthorizationService +import org.forgerock.android.auth.FRUser.Browser + +/** + * This class is an implementation of the ActivityResultContract. + * It is used to handle the OAuth2 authorization process. + */ +internal class AuthorizeContract : ActivityResultContract() { + override fun createIntent( + context: Context, + input: Browser, + ): Intent { + val configurer: AppAuthConfigurer = input.appAuthConfigurer + val oAuth2Client = Config.getInstance().oAuth2Client + + val configuration = configurer.authorizationServiceConfigurationSupplier.get() + val authRequestBuilder = + AuthorizationRequest.Builder( + configuration, + oAuth2Client.clientId, + oAuth2Client.responseType, + Uri.parse(oAuth2Client.redirectUri), + ).setScope(oAuth2Client.scope) + + //Allow caller to override Authorization Request setting + configurer.authorizationRequestBuilder.accept(authRequestBuilder) + val authorizationRequest = authRequestBuilder.build() + + //Allow caller to override AppAuth default setting + val appAuthConfigurationBuilder = AppAuthConfiguration.Builder() + configurer.appAuthConfigurationBuilder.accept(appAuthConfigurationBuilder) + val authorizationService = AuthorizationService(context, appAuthConfigurationBuilder.build()) + + //Allow caller to override custom tabs default setting + val intentBuilder: CustomTabsIntent.Builder = + authorizationService.createCustomTabsIntentBuilder(authorizationRequest.toUri()) + configurer.customTabsIntentBuilder.accept(intentBuilder) + + val request = authRequestBuilder.build() + val service = AuthorizationService(context, AppAuthConfiguration.DEFAULT) + return service.getAuthorizationRequestIntent(request, intentBuilder.build()) + } + + override fun parseResult( + resultCode: Int, + intent: Intent?, + ): Intent? { + return intent + } +} \ No newline at end of file diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/FRUser.java b/forgerock-auth/src/main/java/org/forgerock/android/auth/FRUser.java index e7c48ad2..aa9cb636 100644 --- a/forgerock-auth/src/main/java/org/forgerock/android/auth/FRUser.java +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/FRUser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -14,15 +14,16 @@ import android.content.pm.ResolveInfo; import android.net.Uri; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; import net.openid.appauth.AuthorizationResponse; import net.openid.appauth.RedirectUriReceiverActivity; +import org.forgerock.android.auth.centralize.BrowserLauncher; import org.forgerock.android.auth.exception.AlreadyAuthenticatedException; import org.forgerock.android.auth.exception.AuthenticationRequiredException; import org.forgerock.android.auth.exception.InvalidRedirectUriException; @@ -31,8 +32,6 @@ import java.util.List; import java.util.concurrent.atomic.AtomicReference; -import lombok.AccessLevel; -import lombok.Getter; import lombok.RequiredArgsConstructor; public class FRUser { @@ -99,7 +98,7 @@ public void revokeAccessToken(FRListener listener) { * Refresh the {@link AccessToken} asynchronously, force token refresh, no matter the stored {@link AccessToken} is expired or not * refresh the token and persist it. * - * @param listener Listener to listen for refresh event. + * @param listener Listener to listen for refresh event. */ @WorkerThread public void refresh(FRListener listener) { @@ -256,9 +255,9 @@ public static Browser browser() { return new Browser(); } - @Getter(AccessLevel.PACKAGE) public static class Browser { + private static final String TAG = Browser.class.getName(); private FRListener listener; private AppAuthConfigurer appAuthConfigurer = new AppAuthConfigurer(this); private boolean failedOnNoBrowserFound = true; @@ -281,7 +280,7 @@ public AppAuthConfigurer appAuthConfigurer() { * throws {@link java.net.MalformedURLException} When failed to parse the URL for API request. */ public void login(Fragment fragment, FRListener listener) { - login(fragment.getContext(), fragment.getFragmentManager(), listener); + login(fragment.getActivity(), listener); } /** @@ -298,17 +297,6 @@ public void login(Fragment fragment, FRListener listener) { * throws {@link java.net.MalformedURLException} When failed to parse the URL for API request. */ public void login(FragmentActivity activity, FRListener listener) { - login(activity.getApplicationContext(), activity.getSupportFragmentManager(), listener); - } - - @VisibleForTesting - Browser failedOnNoBrowserFound(boolean failedOnNoBrowserFound) { - this.failedOnNoBrowserFound = failedOnNoBrowserFound; - return this; - } - - private void login(Context context, FragmentManager manager, FRListener listener) { - SessionManager sessionManager = Config.getInstance().getSessionManager(); if (sessionManager.hasSession()) { @@ -317,17 +305,17 @@ private void login(Context context, FragmentManager manager, FRListener } try { - validateRedirectUri(context); + validateRedirectUri(activity); } catch (InvalidRedirectUriException e) { Listener.onException(listener, e); return; } - this.listener = new FRListener() { + this.listener = new FRListener<>() { @Override public void onSuccess(AuthorizationResponse result) { InterceptorHandler interceptorHandler = InterceptorHandler.builder() - .context(context) + .context(activity) .listener(listener) .interceptor(new ExchangeAccessTokenInterceptor(sessionManager.getTokenManager())) .interceptor(new AccessTokenStoreInterceptor(sessionManager.getTokenManager())) @@ -335,17 +323,15 @@ public void onSuccess(AuthorizationResponse result) { .build(); interceptorHandler.proceed(result); - } @Override - public void onException(Exception e) { + public void onException(@NonNull Exception e) { Listener.onException(listener, e); } }; - AppAuthFragment.launch(manager, this); - + AppAuthFragment2.launch(activity, this); } private void validateRedirectUri(Context context) throws InvalidRedirectUriException { @@ -359,7 +345,7 @@ private void validateRedirectUri(Context context) throws InvalidRedirectUriExcep intent.setData(uri); resolveInfos = pm.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER); } - if (resolveInfos != null && resolveInfos.size() > 0) { + if (resolveInfos != null && !resolveInfos.isEmpty()) { for (ResolveInfo info : resolveInfos) { ActivityInfo activityInfo = info.activityInfo; if (!(activityInfo.name.equals(RedirectUriReceiverActivity.class.getCanonicalName()) && @@ -371,5 +357,23 @@ private void validateRedirectUri(Context context) throws InvalidRedirectUriExcep } throw new InvalidRedirectUriException("No App is registered to capture the authorization code"); } + + @VisibleForTesting + Browser failedOnNoBrowserFound(boolean failedOnNoBrowserFound) { + this.failedOnNoBrowserFound = failedOnNoBrowserFound; + return this; + } + + FRListener getListener() { + return this.listener; + } + + AppAuthConfigurer getAppAuthConfigurer() { + return this.appAuthConfigurer; + } + + boolean isFailedOnNoBrowserFound() { + return this.failedOnNoBrowserFound; + } } } diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java b/forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java index 76d22c18..8cc4b039 100644 --- a/forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -13,7 +13,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.forgerock.android.auth.exception.ApiException; import org.forgerock.android.auth.exception.AuthorizeException; import org.jetbrains.annotations.NotNull; @@ -27,7 +26,6 @@ import java.security.SecureRandom; import java.util.Map; -import lombok.Getter; import okhttp3.Call; import okhttp3.Callback; import okhttp3.FormBody; @@ -41,7 +39,6 @@ /** * Class to handle OAuth2 related endpoint */ -@Getter public class OAuth2Client { private static final String TAG = "OAuth2Client"; @@ -63,7 +60,6 @@ public class OAuth2Client { private String redirectUri; private String responseType = OAuth2.CODE; - @Getter private ServerConfig serverConfig; private OkHttpClient okHttpClient; @@ -109,7 +105,7 @@ public void exchangeToken(@NonNull SSOToken token, .url(getAuthorizeUrl(token, pkce, state, additionalParameters)) .get() .header(ACCEPT_API_VERSION, ServerConfig.API_VERSION_2_1) - .header(serverConfig.getCookieName(), token.getValue() ) + .header(serverConfig.getCookieName(), token.getValue()) .tag(AUTHORIZE) .build(); @@ -219,9 +215,9 @@ public void revoke(@NonNull AccessToken accessToken, final FRListener list * Revoke the AccessToken, to revoke the access token, first look for refresh token to revoke, if * not provided or useRefreshToken = false, will revoke with the access token. * - * @param accessToken The AccessToken to be revoked + * @param accessToken The AccessToken to be revoked * @param useRefreshToken If true, revoke with refresh token, otherwise revoke access token - * @param listener Listener to listen for revoke event + * @param listener Listener to listen for revoke event */ public void revoke(@NonNull AccessToken accessToken, boolean useRefreshToken, final FRListener listener) { Logger.debug(TAG, "Revoking Access Token & Refresh Token"); @@ -459,4 +455,23 @@ private PKCE generateCodeChallenge() throws UnsupportedEncodingException { } } + String getClientId() { + return this.clientId; + } + + String getScope() { + return this.scope; + } + + String getRedirectUri() { + return this.redirectUri; + } + + String getResponseType() { + return this.responseType; + } + + ServerConfig getServerConfig() { + return this.serverConfig; + } } diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/CallbackFactory.java b/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/CallbackFactory.java deleted file mode 100644 index 7ca0a0d3..00000000 --- a/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/CallbackFactory.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2019 - 2023 ForgeRock. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -package org.forgerock.android.auth.callback; - -import org.forgerock.android.auth.Logger; - -import java.util.HashMap; -import java.util.Map; - -/** - * Factory to manage supported {@link Callback} - */ -public class CallbackFactory { - - private static final String TAG = CallbackFactory.class.getSimpleName(); - private static final CallbackFactory INSTANCE = new CallbackFactory(); - - private Map> callbacks = new HashMap<>(); - - private CallbackFactory() { - register(ChoiceCallback.class); - register(NameCallback.class); - register(PasswordCallback.class); - register(StringAttributeInputCallback.class); - register(NumberAttributeInputCallback.class); - register(BooleanAttributeInputCallback.class); - register(ValidatedPasswordCallback.class); - register(ValidatedUsernameCallback.class); - register(KbaCreateCallback.class); - register(TermsAndConditionsCallback.class); - register(PollingWaitCallback.class); - register(ConfirmationCallback.class); - register(TextOutputCallback.class); - register(SuspendedTextOutputCallback.class); - register(ReCaptchaCallback.class); - register(ConsentMappingCallback.class); - register(HiddenValueCallback.class); - register(DeviceProfileCallback.class); - register(MetadataCallback.class); - register(WebAuthnRegistrationCallback.class); - register(WebAuthnAuthenticationCallback.class); - register(SelectIdPCallback.class); - register(IdPCallback.class); - register(DeviceBindingCallback.class); - register(DeviceSigningVerifierCallback.class); - register(AppIntegrityCallback.class); - } - - /** - * Returns a cached instance {@link CallbackFactory} - * - * @return instance of {@link CallbackFactory} - */ - public static CallbackFactory getInstance() { - return INSTANCE; - } - - /** - * Register new Callback Class - * - * @param callback The callback Class - */ - public void register(Class callback) { - try { - callbacks.put(getType(callback), callback); - } catch (Exception e) { - Logger.error(TAG, e, e.getMessage()); - } - } - - public String getType(Class callback) throws InstantiationException, IllegalAccessException { - return callback.newInstance().getType(); - } - - public Map> getCallbacks() { - return this.callbacks; - } -} diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/CallbackFactory.kt b/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/CallbackFactory.kt new file mode 100644 index 00000000..213ecebe --- /dev/null +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/CallbackFactory.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth.callback + +import org.forgerock.android.auth.Logger.Companion.error + +/** + * Factory to manage supported [Callback] + */ +class CallbackFactory private constructor() { + internal val callbacks: MutableMap> = HashMap() + + init { + register(ChoiceCallback::class.java) + register(NameCallback::class.java) + register(PasswordCallback::class.java) + register(StringAttributeInputCallback::class.java) + register(NumberAttributeInputCallback::class.java) + register(BooleanAttributeInputCallback::class.java) + register(ValidatedPasswordCallback::class.java) + register(ValidatedUsernameCallback::class.java) + register(KbaCreateCallback::class.java) + register(TermsAndConditionsCallback::class.java) + register(PollingWaitCallback::class.java) + register(ConfirmationCallback::class.java) + register(TextOutputCallback::class.java) + register(SuspendedTextOutputCallback::class.java) + register(ReCaptchaCallback::class.java) + register(ConsentMappingCallback::class.java) + register(HiddenValueCallback::class.java) + register(DeviceProfileCallback::class.java) + register(MetadataCallback::class.java) + register(WebAuthnRegistrationCallback::class.java) + register(WebAuthnAuthenticationCallback::class.java) + register(SelectIdPCallback::class.java) + register(IdPCallback::class.java) + register(DeviceBindingCallback::class.java) + register(DeviceSigningVerifierCallback::class.java) + register(AppIntegrityCallback::class.java) + register(TextInputCallback::class.java) + } + + /** + * Register new Callback Class + * + * @param callback The callback Class + */ + fun register(callback: Class) { + try { + callbacks[getType(callback)] = callback + } catch (e: Exception) { + error(TAG, e, e.message) + } + } + + @Throws(InstantiationException::class, IllegalAccessException::class) + fun getType(callback: Class): String { + return callback.getDeclaredConstructor().newInstance().type + } + + fun getCallbacks(): Map> { + return callbacks + } + + companion object { + private val TAG = CallbackFactory::class.java.simpleName + + private val INSTANCE = CallbackFactory() + + /** + * Returns a cached instance [CallbackFactory] + * + * @return instance of [CallbackFactory] + */ + @JvmStatic fun getInstance(): CallbackFactory { + return INSTANCE + } + + } +} diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/DeviceBindingCallback.kt b/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/DeviceBindingCallback.kt index 0df9c831..367b6f60 100644 --- a/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/DeviceBindingCallback.kt +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/DeviceBindingCallback.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2022 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -170,15 +170,17 @@ open class DeviceBindingCallback : AbstractCallback, Binding { * @param context The Application Context * @param deviceAuthenticator A function to return a [DeviceAuthenticator], [deviceAuthenticatorIdentifier] will be used if not provided * @param listener The Listener to listen for the result + * @param prompt The Prompt to modify the title, subtitle, description */ @JvmOverloads open fun bind(context: Context, deviceAuthenticator: (type: DeviceBindingAuthenticationType) -> DeviceAuthenticator = deviceAuthenticatorIdentifier, - listener: FRListener) { + listener: FRListener, + prompt: Prompt? = null) { val scope = CoroutineScope(Dispatchers.Default) scope.launch { try { - bind(context, deviceAuthenticator) + bind(context, prompt = prompt, deviceAuthenticator = deviceAuthenticator) Listener.onSuccess(listener, null) } catch (e: Exception) { Listener.onException(listener, e) @@ -192,11 +194,13 @@ open class DeviceBindingCallback : AbstractCallback, Binding { * keys before calling this method * * @param context The Application Context + * @param prompt The Prompt to modify the title, subtitle, description * @param deviceAuthenticator A function to return a [DeviceAuthenticator], [deviceAuthenticatorIdentifier] will be used if not provided */ open suspend fun bind(context: Context, + prompt: Prompt? = null, deviceAuthenticator: (type: DeviceBindingAuthenticationType) -> DeviceAuthenticator = deviceAuthenticatorIdentifier) { - execute(context, deviceAuthenticator(deviceBindingAuthenticationType)) + execute(context, deviceAuthenticator(deviceBindingAuthenticationType), prompt = prompt) } @@ -206,6 +210,8 @@ open class DeviceBindingCallback : AbstractCallback, Binding { * @param context The Application Context * @param deviceAuthenticator Interface to find the Authentication Type * @param deviceBindingRepository Persist the values in encrypted shared preference + * @param deviceId Generated Device Identifier + * @param prompt The Prompt to modify the title, subtitle, description */ internal suspend fun execute(context: Context, deviceAuthenticator: DeviceAuthenticator = getDeviceAuthenticator( @@ -213,9 +219,10 @@ open class DeviceBindingCallback : AbstractCallback, Binding { deviceBindingRepository: DeviceBindingRepository = LocalDeviceBindingRepository( context), deviceId: String = DeviceIdentifier.builder().context(context) - .build().identifier) { + .build().identifier, + prompt: Prompt? = null) { - deviceAuthenticator.initialize(userId, Prompt(title, subtitle, description)) + deviceAuthenticator.initialize(userId, prompt ?: Prompt(title, subtitle, description)) if (deviceAuthenticator.isSupported(context, attestation).not()) { handleException(DeviceBindingException(Unsupported())) diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/DeviceSigningVerifierCallback.kt b/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/DeviceSigningVerifierCallback.kt index 8148bd98..0c73f277 100644 --- a/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/DeviceSigningVerifierCallback.kt +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/DeviceSigningVerifierCallback.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2022 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -118,18 +118,20 @@ open class DeviceSigningVerifierCallback : AbstractCallback, Binding { * Sign the challenge with bounded device keys. * * @param context The Application Context - * @param userKeySelector Collect user key, if not provided [DefaultUserKeySelector] will be used * @param customClaims A map of custom claims to be added to the jws payload + * @param prompt The Prompt to modify the title, subtitle, description + * @param userKeySelector Collect user key, if not provided [DefaultUserKeySelector] will be used * @param deviceAuthenticator A function to return a [DeviceAuthenticator], [deviceAuthenticatorIdentifier] will be used if not provided */ open suspend fun sign(context: Context, customClaims: Map = emptyMap(), + prompt: Prompt? = null, userKeySelector: UserKeySelector = DefaultUserKeySelector(), deviceAuthenticator: (type: DeviceBindingAuthenticationType) -> DeviceAuthenticator = deviceAuthenticatorIdentifier) { execute(context, userKeySelector = userKeySelector, deviceAuthenticator = deviceAuthenticator, - customClaims = customClaims) + customClaims = customClaims, prompt = prompt) } @@ -137,21 +139,23 @@ open class DeviceSigningVerifierCallback : AbstractCallback, Binding { * Sign the challenge with bounded device keys. * * @param context The Application Context - * @param userKeySelector Collect user key, if not provided [DefaultUserKeySelector] will be used * @param customClaims A map of custom claims to be added to the jws payload + * @param userKeySelector Collect user key, if not provided [DefaultUserKeySelector] will be used * @param deviceAuthenticator A function to return a [DeviceAuthenticator], [deviceAuthenticatorIdentifier] will be used if not provided * @param listener The Listener to listen for the result + * @param prompt The Prompt to modify the title, subtitle, description */ @JvmOverloads open fun sign(context: Context, customClaims: Map = emptyMap(), userKeySelector: UserKeySelector = DefaultUserKeySelector(), deviceAuthenticator: (type: DeviceBindingAuthenticationType) -> DeviceAuthenticator = deviceAuthenticatorIdentifier, - listener: FRListener) { + listener: FRListener, + prompt: Prompt? = null) { val scope = CoroutineScope(Dispatchers.Default) scope.launch { try { - sign(context, customClaims, userKeySelector, deviceAuthenticator) + sign(context, customClaims, prompt, userKeySelector, deviceAuthenticator) Listener.onSuccess(listener, null) } catch (e: Exception) { Listener.onException(listener, e) @@ -166,13 +170,15 @@ open class DeviceSigningVerifierCallback : AbstractCallback, Binding { * @param userKey User Information * @param deviceAuthenticator A function to return a [DeviceAuthenticator], [getDeviceAuthenticator] will be used if not provided * @param customClaims A map of custom claims to be added to the jws payload + * @param prompt The Prompt to modify the title, subtitle, description */ protected open suspend fun authenticate(context: Context, userKey: UserKey, deviceAuthenticator: DeviceAuthenticator, - customClaims: Map = emptyMap()) { + customClaims: Map = emptyMap(), + prompt: Prompt? = null) { - deviceAuthenticator.initialize(userKey.userId, Prompt(title, subtitle, description)) + deviceAuthenticator.initialize(userKey.userId, prompt?: Prompt(title, subtitle, description)) if (deviceAuthenticator.isSupported(context).not()) { handleException(DeviceBindingException(Unsupported())) @@ -206,7 +212,8 @@ open class DeviceSigningVerifierCallback : AbstractCallback, Binding { userKeyService: UserKeyService = UserDeviceKeyService(context), userKeySelector: UserKeySelector = DefaultUserKeySelector(), deviceAuthenticator: (type: DeviceBindingAuthenticationType) -> DeviceAuthenticator, - customClaims: Map = emptyMap()) { + customClaims: Map = emptyMap(), + prompt: Prompt? = null) { try { withTimeout(getDuration(timeout)) { @@ -215,7 +222,8 @@ open class DeviceSigningVerifierCallback : AbstractCallback, Binding { is SingleKeyFound -> authenticate(context, status.key, deviceAuthenticator(status.key.authType), - customClaims) + customClaims, + prompt) else -> { val userKey = userKeySelector.selectUserKey(UserKeys(userKeyService.getAll())) diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/TextInputCallback.kt b/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/TextInputCallback.kt new file mode 100644 index 00000000..85b87471 --- /dev/null +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/callback/TextInputCallback.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package org.forgerock.android.auth.callback + +import androidx.annotation.Keep +import org.json.JSONObject + +/** + * Callback for collection of a single text input attribute from a user. + * + * + */ +class TextInputCallback : AbstractPromptCallback { + + /** + * TextInputCallback sample. + * + * { + * "type": "TextInputCallback", + * "output": [ + * { + * "name": "prompt", + * "value": "Text input" + * }, + * { + * "name": "defaultText", + * "value": "" + * } + * ], + * "input": [ + * { + * "name": "IDToken1", + * "value": "" + * } + * ] + * } + * + */ + + /** + * The text to be used as the default text displayed with the prompt. + */ + var defaultText: String? = null + private set + + @Keep + @JvmOverloads + constructor() : super() + + @Keep + @JvmOverloads + constructor(raw: JSONObject?, index: Int) : super(raw, index) + + override fun setAttribute(name: String, value: Any) { + super.setAttribute(name, value) + when (name) { + "defaultText" -> defaultText = value as String + else -> {} + } + } + + /** + * Set the text. + * @param value the text, which may be null. + */ + fun setValue(value: String?) { + super.setValue(value) + } + + override fun getType(): String { + return "TextInputCallback" + } +} \ No newline at end of file diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/centralize/BrowserLauncher.kt b/forgerock-auth/src/main/java/org/forgerock/android/auth/centralize/BrowserLauncher.kt new file mode 100644 index 00000000..ef4ffdc3 --- /dev/null +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/centralize/BrowserLauncher.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package org.forgerock.android.auth.centralize + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import net.openid.appauth.AuthorizationResponse +import org.forgerock.android.auth.FRListener +import org.forgerock.android.auth.FRUser +import org.forgerock.android.auth.Listener + +/** + * Singleton class to launch the Browser + * Centralize login browser launcher. + */ +internal object BrowserLauncher { + + private var launcher: Launcher? = null + + /** + * Initialize the Launcher + */ + @Synchronized + internal fun init(launcher: Launcher) { + this.launcher = launcher + } + + /** + * reset the Launcher state + */ + @Synchronized + internal fun reset() { + launcher?.authorize?.unregister() + launcher = null + } + + /** + * Authorize the user using the Browser + */ + suspend fun authorize(browser: FRUser.Browser): AuthorizationResponse { + return launcher?.authorize(browser) + ?: throw IllegalStateException("Launcher is not initialized") + } + + /** + * Authorize the user using the Browser + */ + fun authorize(browser: FRUser.Browser, listener: FRListener): Boolean { + return launcher?.let { + val scope = CoroutineScope(Dispatchers.Default) + scope.launch { + try { + val result = authorize(browser) + Listener.onSuccess(listener, result) + } catch (e: Exception) { + Listener.onException(listener, e) + } + } + return true + } ?: false + } +} \ No newline at end of file diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/centralize/Launcher.kt b/forgerock-auth/src/main/java/org/forgerock/android/auth/centralize/Launcher.kt new file mode 100644 index 00000000..c433dfc6 --- /dev/null +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/centralize/Launcher.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package org.forgerock.android.auth.centralize + +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.first +import net.openid.appauth.AuthorizationException +import net.openid.appauth.AuthorizationResponse +import org.forgerock.android.auth.FRUser +import org.forgerock.android.auth.exception.BrowserAuthenticationException + +internal class Launcher(val authorize: ActivityResultLauncher, + val state: MutableStateFlow, + private val pending: Boolean = false) { + suspend fun authorize(request: FRUser.Browser): AuthorizationResponse { + //If waiting for response, we don't launch the browser again + if (!pending) { + authorize.launch(request) + } + //drop the default value + state.drop(1).first().let { + it?.let { i -> + val error = i.getStringExtra(AuthorizationException.EXTRA_EXCEPTION) + error?.let { e -> throw BrowserAuthenticationException(e) } + return AuthorizationResponse.fromIntent(i) + ?: throw BrowserAuthenticationException("Failed to retrieve authorization code") + } + throw BrowserAuthenticationException("No response data") + } + } +} \ No newline at end of file diff --git a/forgerock-auth/src/test/java/org/forgerock/android/auth/BrowserLoginTest.kt b/forgerock-auth/src/test/java/org/forgerock/android/auth/BrowserLoginTest.kt index 7d37d569..5bbe88bf 100644 --- a/forgerock-auth/src/test/java/org/forgerock/android/auth/BrowserLoginTest.kt +++ b/forgerock-auth/src/test/java/org/forgerock/android/auth/BrowserLoginTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2020 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -26,6 +26,7 @@ import org.forgerock.android.auth.exception.ApiException import org.forgerock.android.auth.exception.AuthenticationRequiredException import org.forgerock.android.auth.exception.BrowserAuthenticationException import org.junit.Assert +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -43,6 +44,7 @@ class BrowserLoginTest : BaseTest() { return fragment as? AppAuthFragment } + @Ignore @Test @Throws( InterruptedException::class, @@ -98,6 +100,7 @@ class BrowserLoginTest : BaseTest() { Assertions.assertThat(body["code"]).isEqualTo("roxwkG0TtooR2vzA6z0MT9xyJSQ") } + @Ignore @Test @Throws( InterruptedException::class, @@ -135,6 +138,7 @@ class BrowserLoginTest : BaseTest() { ).isNotNull } + @Ignore @Test @Throws( InterruptedException::class, @@ -175,7 +179,7 @@ class BrowserLoginTest : BaseTest() { } //It is running with JVM, no browser is expected - @Test(expected = ActivityNotFoundException::class) + @Test @Throws( InterruptedException::class, ExecutionException::class, @@ -228,6 +232,7 @@ class BrowserLoginTest : BaseTest() { } + @Ignore @Test @Throws(InterruptedException::class) fun testOperationCancel() { @@ -262,6 +267,7 @@ class BrowserLoginTest : BaseTest() { } } + @Ignore @Test(expected = AlreadyAuthenticatedException::class) @Throws( Throwable::class @@ -281,6 +287,7 @@ class BrowserLoginTest : BaseTest() { } } + @Ignore @Test @Throws(InterruptedException::class) fun testInvalidScope() { @@ -310,6 +317,7 @@ class BrowserLoginTest : BaseTest() { } } + @Ignore @Test @Throws( InterruptedException::class, @@ -351,6 +359,38 @@ class BrowserLoginTest : BaseTest() { Assertions.assertThat(result["END_SESSION"]?.second).isEqualTo(1) } + @Test + fun testActivityNotFound() { + val scenario: ActivityScenario = + ActivityScenario.launch(DummyActivity::class.java) + scenario.onActivity { + InitProvider.setCurrentActivity(it) + } + val future = FRListenerFuture() + scenario.onActivity { + FRUser.browser().failedOnNoBrowserFound(true) + .login(it, future) + } + + val intent = Intent() + intent.putExtra( + AuthorizationResponse.EXTRA_RESPONSE, + "{\"request\":{\"configuration\":{\"authorizationEndpoint\":\"http:\\/\\/openam.example.com:8081\\/openam\\/oauth2\\/realms\\/root\\/authorize\",\"tokenEndpoint\":\"http:\\/\\/openam.example.com:8081\\/openam\\/oauth2\\/realms\\/root\\/access_token\"},\"clientId\":\"AndroidTest\",\"responseType\":\"code\",\"redirectUri\":\"net.openid.appauthdemo2:\\/oauth2redirect\",\"login_hint\":\"login\",\"scope\":\"openid profile email address phone\",\"state\":\"2v0SIhB7UAmsqvnvwR-IKQ\",\"codeVerifier\":\"qvCFoo3tqB-89lYOFjX2ZAMalkKgW_KIcc1tN3hmx3ygOHyYbWT9hKU7rhky6ivB-33exlhyyHHeSJtuVfOobg\",\"codeVerifierChallenge\":\"i-UW4h0UlD_pt1WCYGeP6prmtOkXhyQB_s1itrkV68k\",\"codeVerifierChallengeMethod\":\"S256\",\"additionalParameters\":{}},\"state\":\"2v0SIhB7UAmsqvnvwR-IKQ\",\"code\":\"roxwkG0TtooR2vzA6z0MT9xyJSQ\",\"additional_parameters\":{\"iss\":\"http:\\/\\/openam.example.com:8081\\/openam\\/oauth2\",\"client_id\":\"andy_app\"}}" + ) + scenario.onActivity { + getAppAuthFragment(it)?.onActivityResult( + AppAuthFragment.AUTH_REQUEST_CODE, Activity.RESULT_OK, intent + ) + } + + try { + future.get() + Assert.fail() + } catch (e: ExecutionException) { + Assertions.assertThat(e.cause).isInstanceOf(ActivityNotFoundException::class.java) + } + } + private fun parse(encoded: String): Map { val body = encoded.split("&").toTypedArray() val result: MutableMap = HashMap() diff --git a/forgerock-auth/src/test/java/org/forgerock/android/auth/ServerConfigTest.java b/forgerock-auth/src/test/java/org/forgerock/android/auth/ServerConfigTest.java index d1df9aa1..d38acf9e 100644 --- a/forgerock-auth/src/test/java/org/forgerock/android/auth/ServerConfigTest.java +++ b/forgerock-auth/src/test/java/org/forgerock/android/auth/ServerConfigTest.java @@ -80,7 +80,7 @@ public void testSha256Pinning() throws InterruptedException { ServerConfig serverConfig = ServerConfig.builder() .context(context) .url("https://api.ipify.org") - .pin("HMSZyV3whmhwmQlqNPIlvpQA9AHHQ9aj1CEDqFkAuyE=") + .pin("Pf/hyG+ywUC8g3d1abF9kQn83lY6NwJUTUnqe03UMIY=") .build(); OkHttpClient client = OkHttpClientProvider.getInstance().lookup(serverConfig); @@ -122,7 +122,7 @@ public void testMultiplePinning() throws InterruptedException { ServerConfig serverConfig = ServerConfig.builder() .context(context) .url("https://api.ipify.org") - .pin("HMSZyV3whmhwmQlqNPIlvpQA9AHHQ9aj1CEDqFkAuyE=") + .pin("Pf/hyG+ywUC8g3d1abF9kQn83lY6NwJUTUnqe03UMIY=") .pin("invalid") .build(); @@ -214,7 +214,7 @@ public void testBuildStepWithCustomPin() throws InterruptedException { .context(context) .url("https://api.ipify.org") .buildStep(builder -> builder.certificatePinner( - new CertificatePinner.Builder().add("api.ipify.org", "sha1/40WpRckJNrzdAexnwLvKG7aK3qk=" ).build())) + new CertificatePinner.Builder().add("api.ipify.org", "sha1/KmdlCymwI4zbla1kcWu2fBEwKA8=" ).build())) .build(); OkHttpClient client = OkHttpClientProvider.getInstance().lookup(serverConfig); diff --git a/forgerock-auth/src/test/java/org/forgerock/android/auth/callback/DeviceBindingCallbackTest.kt b/forgerock-auth/src/test/java/org/forgerock/android/auth/callback/DeviceBindingCallbackTest.kt index 1c071fe4..5480b6d0 100644 --- a/forgerock-auth/src/test/java/org/forgerock/android/auth/callback/DeviceBindingCallbackTest.kt +++ b/forgerock-auth/src/test/java/org/forgerock/android/auth/callback/DeviceBindingCallbackTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2022 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -16,24 +16,29 @@ import org.forgerock.android.auth.CryptoKey import org.forgerock.android.auth.DummyActivity import org.forgerock.android.auth.InitProvider import org.forgerock.android.auth.devicebind.ApplicationPinDeviceAuthenticator +import org.forgerock.android.auth.devicebind.BiometricAndDeviceCredential +import org.forgerock.android.auth.devicebind.BiometricBindingHandler import org.forgerock.android.auth.devicebind.BiometricOnly import org.forgerock.android.auth.devicebind.DeviceAuthenticator import org.forgerock.android.auth.devicebind.DeviceBindingErrorStatus.* import org.forgerock.android.auth.devicebind.DeviceBindingException import org.forgerock.android.auth.devicebind.DeviceBindingRepository import org.forgerock.android.auth.devicebind.KeyPair +import org.forgerock.android.auth.devicebind.LocalDeviceBindingRepository import org.forgerock.android.auth.devicebind.None import org.forgerock.android.auth.devicebind.PinCollector import org.forgerock.android.auth.devicebind.Prompt -import org.forgerock.android.auth.devicebind.LocalDeviceBindingRepository import org.forgerock.android.auth.devicebind.Success import org.json.JSONObject import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.KArgumentCaptor import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -69,7 +74,7 @@ class DeviceBindingCallbackTest { } @Test - fun testSetDeviceNameAndJWSAndClientError() { + fun testConfig() { val rawContent = "{\"type\":\"DeviceBindingCallback\",\"output\":[{\"name\":\"userId\",\"value\":\"id=demo,ou=user,dc=openam,dc=forgerock,dc=org\"},{\"name\":\"username\",\"value\":\"demo\"},{\"name\":\"authenticationType\",\"value\":\"APPLICATION_PIN\"},{\"name\":\"challenge\",\"value\":\"CS3+g40VkHXx+dN7rpnJKhrEAvwZaYgbaXoEcpO5twM=\"},{\"name\":\"title\",\"value\":\"Authentication required\"},{\"name\":\"subtitle\",\"value\":\"Cryptography device binding\"},{\"name\":\"description\",\"value\":\"Please complete with biometric to proceed\"},{\"name\":\"timeout\",\"value\":60},{\"name\":\"attestation\",\"value\":false}],\"input\":[{\"name\":\"IDToken1jws\",\"value\":\"\"},{\"name\":\"IDToken1deviceName\",\"value\":\"\"},{\"name\":\"IDToken1deviceId\",\"value\":\"\"},{\"name\":\"IDToken1clientError\",\"value\":\"\"}]}"; val obj = JSONObject(rawContent) @@ -87,7 +92,7 @@ class DeviceBindingCallbackTest { @Test fun testSuccessPathForNoneType() = runBlocking { val rawContent = - "{\"type\":\"DeviceBindingCallback\",\"output\":[{\"name\":\"userId\",\"value\":\"id=demo,ou=user,dc=openam,dc=forgerock,dc=org\"},{\"name\":\"username\",\"value\":\"demo\"},{\"name\":\"authenticationType\",\"value\":\"APPLICATION_PIN\"},{\"name\":\"challenge\",\"value\":\"CS3+g40VkHXx+dN7rpnJKhrEAvwZaYgbaXoEcpO5twM=\"},{\"name\":\"title\",\"value\":\"Authentication required\"},{\"name\":\"subtitle\",\"value\":\"Cryptography device binding\"},{\"name\":\"description\",\"value\":\"Please complete with biometric to proceed\"},{\"name\":\"timeout\",\"value\":60},{\"name\":\"attestation\",\"value\":false}],\"input\":[{\"name\":\"IDToken1jws\",\"value\":\"\"},{\"name\":\"IDToken1deviceName\",\"value\":\"\"},{\"name\":\"IDToken1deviceId\",\"value\":\"\"},{\"name\":\"IDToken1clientError\",\"value\":\"\"}]}"; + "{\"type\":\"DeviceBindingCallback\",\"output\":[{\"name\":\"userId\",\"value\":\"id=demo,ou=user,dc=openam,dc=forgerock,dc=org\"},{\"name\":\"username\",\"value\":\"demo\"},{\"name\":\"authenticationType\",\"value\":\"NONE\"},{\"name\":\"challenge\",\"value\":\"CS3+g40VkHXx+dN7rpnJKhrEAvwZaYgbaXoEcpO5twM=\"},{\"name\":\"title\",\"value\":\"Authentication required\"},{\"name\":\"subtitle\",\"value\":\"Cryptography device binding\"},{\"name\":\"description\",\"value\":\"Please complete with biometric to proceed\"},{\"name\":\"timeout\",\"value\":60},{\"name\":\"attestation\",\"value\":false}],\"input\":[{\"name\":\"IDToken1jws\",\"value\":\"\"},{\"name\":\"IDToken1deviceName\",\"value\":\"\"},{\"name\":\"IDToken1deviceId\",\"value\":\"\"},{\"name\":\"IDToken1clientError\",\"value\":\"\"}]}"; val encryptedPref = mock() val deviceAuthenticator = mock() whenever(deviceAuthenticator.isSupported(any(), any())).thenReturn(true) @@ -110,7 +115,7 @@ class DeviceBindingCallbackTest { @Test fun testSuccessPathForBiometricType() = runBlocking { val rawContent = - "{\"type\":\"DeviceBindingCallback\",\"output\":[{\"name\":\"userId\",\"value\":\"id=demo,ou=user,dc=openam,dc=forgerock,dc=org\"},{\"name\":\"username\",\"value\":\"demo\"},{\"name\":\"authenticationType\",\"value\":\"APPLICATION_PIN\"},{\"name\":\"challenge\",\"value\":\"CS3+g40VkHXx+dN7rpnJKhrEAvwZaYgbaXoEcpO5twM=\"},{\"name\":\"title\",\"value\":\"Authentication required\"},{\"name\":\"subtitle\",\"value\":\"Cryptography device binding\"},{\"name\":\"description\",\"value\":\"Please complete with biometric to proceed\"},{\"name\":\"timeout\",\"value\":60},{\"name\":\"attestation\",\"value\":false}],\"input\":[{\"name\":\"IDToken1jws\",\"value\":\"\"},{\"name\":\"IDToken1deviceName\",\"value\":\"\"},{\"name\":\"IDToken1deviceId\",\"value\":\"\"},{\"name\":\"IDToken1clientError\",\"value\":\"\"}]}"; + "{\"type\":\"DeviceBindingCallback\",\"output\":[{\"name\":\"userId\",\"value\":\"id=demo,ou=user,dc=openam,dc=forgerock,dc=org\"},{\"name\":\"username\",\"value\":\"demo\"},{\"name\":\"authenticationType\",\"value\":\"BIOMETRIC_ONLY\"},{\"name\":\"challenge\",\"value\":\"CS3+g40VkHXx+dN7rpnJKhrEAvwZaYgbaXoEcpO5twM=\"},{\"name\":\"title\",\"value\":\"Authentication required\"},{\"name\":\"subtitle\",\"value\":\"Cryptography device binding\"},{\"name\":\"description\",\"value\":\"Please complete with biometric to proceed\"},{\"name\":\"timeout\",\"value\":60},{\"name\":\"attestation\",\"value\":false}],\"input\":[{\"name\":\"IDToken1jws\",\"value\":\"\"},{\"name\":\"IDToken1deviceName\",\"value\":\"\"},{\"name\":\"IDToken1deviceId\",\"value\":\"\"},{\"name\":\"IDToken1clientError\",\"value\":\"\"}]}"; val deviceAuthenticator = mock() whenever(deviceAuthenticator.type()).thenReturn(DeviceBindingAuthenticationType.BIOMETRIC_ONLY) whenever(deviceAuthenticator.isSupported(any(), any())).thenReturn(true) @@ -133,14 +138,26 @@ class DeviceBindingCallbackTest { deviceAuthenticator = deviceAuthenticator, encryptedPreference = encryptedPref, "device_id") + val captor: KArgumentCaptor = argumentCaptor() verify(deviceAuthenticator).setKey(any()) + verify(deviceAuthenticator).prompt(captor.capture()) + assertTrue(captor.firstValue.title == "Authentication required") + assertTrue(captor.firstValue.subtitle == "Cryptography device binding") + assertTrue(captor.firstValue.description == "Please complete with biometric to proceed") verify(deviceAuthenticator).setBiometricHandler(any()) } @Test fun testSuccessPathForBiometricAndCredentialType() = runBlocking { val rawContent = - "{\"type\":\"DeviceBindingCallback\",\"output\":[{\"name\":\"userId\",\"value\":\"id=demo,ou=user,dc=openam,dc=forgerock,dc=org\"},{\"name\":\"username\",\"value\":\"demo\"},{\"name\":\"authenticationType\",\"value\":\"APPLICATION_PIN\"},{\"name\":\"challenge\",\"value\":\"CS3+g40VkHXx+dN7rpnJKhrEAvwZaYgbaXoEcpO5twM=\"},{\"name\":\"title\",\"value\":\"Authentication required\"},{\"name\":\"subtitle\",\"value\":\"Cryptography device binding\"},{\"name\":\"description\",\"value\":\"Please complete with biometric to proceed\"},{\"name\":\"timeout\",\"value\":60},{\"name\":\"attestation\",\"value\":false}],\"input\":[{\"name\":\"IDToken1jws\",\"value\":\"\"},{\"name\":\"IDToken1deviceName\",\"value\":\"\"},{\"name\":\"IDToken1deviceId\",\"value\":\"\"},{\"name\":\"IDToken1clientError\",\"value\":\"\"}]}"; + "{\"type\":\"DeviceBindingCallback\",\"output\":[{\"name\":\"userId\",\"value\":\"id=demo,ou=user,dc=openam,dc=forgerock,dc=org\"},{\"name\":\"username\",\"value\":\"demo\"},{\"name\":\"authenticationType\",\"value\":\"BIOMETRIC_ALLOW_FALLBACK\"},{\"name\":\"challenge\",\"value\":\"CS3+g40VkHXx+dN7rpnJKhrEAvwZaYgbaXoEcpO5twM=\"},{\"name\":\"title\",\"value\":\"Authentication required\"},{\"name\":\"subtitle\",\"value\":\"Cryptography device binding\"},{\"name\":\"description\",\"value\":\"Please complete with biometric to proceed\"},{\"name\":\"timeout\",\"value\":60},{\"name\":\"attestation\",\"value\":false}],\"input\":[{\"name\":\"IDToken1jws\",\"value\":\"\"},{\"name\":\"IDToken1deviceName\",\"value\":\"\"},{\"name\":\"IDToken1deviceId\",\"value\":\"\"},{\"name\":\"IDToken1clientError\",\"value\":\"\"}]}"; + val scenario: ActivityScenario = + ActivityScenario.launch(DummyActivity::class.java) + scenario.onActivity { + InitProvider.setCurrentActivity(it) + } + val deviceAuthenticator = mock() + whenever(deviceAuthenticator.type()).thenReturn(DeviceBindingAuthenticationType.BIOMETRIC_ALLOW_FALLBACK) whenever(deviceAuthenticator.isSupported(any(), any())).thenReturn(true) whenever(deviceAuthenticator.generateKeys(any(), any())).thenReturn(keyPair) whenever(deviceAuthenticator.authenticate(any())).thenReturn(Success(keyPair.privateKey)) @@ -150,11 +167,16 @@ class DeviceBindingCallbackTest { challenge, getExpiration())).thenReturn("signedJWT") + val prompt = Prompt("test1", "test2", "test3") val testObject = DeviceBindingCallbackMockTest(rawContent) + val captor: KArgumentCaptor = argumentCaptor() testObject.testExecute(context, deviceAuthenticator = deviceAuthenticator, encryptedPreference = encryptedPref, - "device_id") + "device_id", prompt = prompt) + verify(deviceAuthenticator).prompt(prompt) + verify(deviceAuthenticator).setBiometricHandler(captor.capture()) + assertNotNull(captor.firstValue) } @Test @@ -333,14 +355,16 @@ class DeviceBindingCallbackMockTest constructor(rawContent: String, suspend fun testExecute(context: Context, deviceAuthenticator: DeviceAuthenticator, encryptedPreference: DeviceBindingRepository, - deviceId: String) { - execute(context, deviceAuthenticator, encryptedPreference, deviceId) + deviceId: String, + prompt: Prompt? = null) { + execute(context, deviceAuthenticator, encryptedPreference, deviceId, prompt) } suspend fun testExecute(context: Context, encryptedPreference: DeviceBindingRepository, - deviceId: String) { - execute(context, deviceBindingRepository = encryptedPreference, deviceId = deviceId) + deviceId: String, + prompt: Prompt? = null) { + execute(context, deviceBindingRepository = encryptedPreference, deviceId = deviceId, prompt = prompt) } override fun getCryptoKey(): CryptoKey { diff --git a/forgerock-auth/src/test/java/org/forgerock/android/auth/callback/DeviceSigningVerifierCallbackTest.kt b/forgerock-auth/src/test/java/org/forgerock/android/auth/callback/DeviceSigningVerifierCallbackTest.kt index 5e817912..55a931cb 100644 --- a/forgerock-auth/src/test/java/org/forgerock/android/auth/callback/DeviceSigningVerifierCallbackTest.kt +++ b/forgerock-auth/src/test/java/org/forgerock/android/auth/callback/DeviceSigningVerifierCallbackTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2022 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -18,6 +18,7 @@ import org.forgerock.android.auth.devicebind.DeviceBindingException import org.forgerock.android.auth.devicebind.KeyPair import org.forgerock.android.auth.devicebind.MultipleKeysFound import org.forgerock.android.auth.devicebind.NoKeysFound +import org.forgerock.android.auth.devicebind.Prompt import org.forgerock.android.auth.devicebind.SingleKeyFound import org.forgerock.android.auth.devicebind.Success import org.forgerock.android.auth.devicebind.UserKey @@ -29,8 +30,11 @@ import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.KArgumentCaptor import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.security.PrivateKey import java.security.interfaces.RSAPublicKey @@ -95,9 +99,11 @@ class DeviceSigningVerifierCallbackTest { "zYwKaKnqS2YzvhXSK+sFjC7FKBoprArqz6LpJ8qe9+g=", getExpiration())).thenReturn("jws") + val prompt = Prompt("test1", "test2", "test3") val testObject = DeviceSigningVerifierCallbackMock(rawContent) - testObject.executeAuthenticate(context, userKey, deviceAuthenticator) + testObject.executeAuthenticate(context, userKey, deviceAuthenticator, prompt = prompt) + verify(deviceAuthenticator).prompt(prompt) } @Test @@ -130,6 +136,12 @@ class DeviceSigningVerifierCallbackTest { val key = UserKey("id1", "jey", "jey", "kid", DeviceBindingAuthenticationType.NONE, System.currentTimeMillis()) testObject.executeAuthenticate(context, key, deviceAuthenticator) + val captor: KArgumentCaptor = argumentCaptor() + verify(deviceAuthenticator).prompt(captor.capture()) + Assert.assertTrue(captor.firstValue.title == "Authentication required") + Assert.assertTrue(captor.firstValue.subtitle == "Cryptography device binding") + Assert.assertTrue(captor.firstValue.description == "Please complete with biometric to proceed") + } @Test(expected = DeviceBindingException::class ) @@ -219,17 +231,18 @@ class DeviceSigningVerifierCallbackMock constructor(rawContent: String, suspend fun executeAuthenticate(context: Context, userKey: UserKey, - authInterface: DeviceAuthenticator) { - authenticate(context, userKey, authInterface) + authInterface: DeviceAuthenticator, + prompt: Prompt? = null) { + authenticate(context, userKey, authInterface, prompt = prompt) } suspend fun executeAllKey(context: Context, - userKeyService: UserKeyService, authenticator: (DeviceBindingAuthenticationType) -> DeviceAuthenticator) { + userKeyService: UserKeyService, prompt: Prompt? = null, authenticator: (DeviceBindingAuthenticationType) -> DeviceAuthenticator) { super.execute(context, userKeyService, userKeySelector = object : UserKeySelector { override suspend fun selectUserKey(userKeys: UserKeys, fragmentActivity: FragmentActivity): UserKey { return UserKey("id1" , "jey", "jey", "kid", DeviceBindingAuthenticationType.NONE, System.currentTimeMillis()) } - }, deviceAuthenticator = authenticator, customClaims = emptyMap()) + }, deviceAuthenticator = authenticator, customClaims = emptyMap(), prompt = prompt) } } \ No newline at end of file diff --git a/forgerock-auth/src/test/java/org/forgerock/android/auth/callback/TextInputCallbackTest.kt b/forgerock-auth/src/test/java/org/forgerock/android/auth/callback/TextInputCallbackTest.kt new file mode 100644 index 00000000..7f8871aa --- /dev/null +++ b/forgerock-auth/src/test/java/org/forgerock/android/auth/callback/TextInputCallbackTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package org.forgerock.android.auth.callback + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.json.JSONException +import org.json.JSONObject +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TextInputCallbackTest { + @Test + @Throws(JSONException::class) + fun basicTest() { + val raw = JSONObject("""{ + "type": "TextInputCallback", + "output": [ + { + "name": "prompt", + "value": "One Time Pin" + }, + { + "name": "defaultText", + "value": "" + } + ], + "input": [ + { + "name": "IDToken1", + "value": "" + } + ], + "_id": 0 + }""") + val textInputCallback = TextInputCallback(raw, 0) + Assert.assertEquals("One Time Pin", textInputCallback.getPrompt()) + Assert.assertEquals("", textInputCallback.defaultText) + textInputCallback.setValue("010101") + Assert.assertEquals((textInputCallback.contentAsJson.getJSONArray("input")[0] as JSONObject).getString( + "value"), + "010101") + Assert.assertEquals(0, textInputCallback.get_id().toLong()) + } +} \ No newline at end of file diff --git a/forgerock-authenticator/build.gradle b/forgerock-authenticator/build.gradle deleted file mode 100644 index 11ce6c3d..00000000 --- a/forgerock-authenticator/build.gradle +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2020 - 2023 ForgeRock. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -apply plugin: 'com.android.library' -apply plugin: "com.adarshr.test-logger" -apply plugin: 'maven-publish' -apply plugin: 'signing' -apply plugin: 'kotlin-android' -// We cannot use kdoc for this project due to Lombak ,so need to add this dokka plugin here. -apply plugin: 'org.jetbrains.dokka' - -android { - namespace 'org.forgerock.android.authenticator' - - compileSdk 34 - - useLibrary 'android.test.base' - useLibrary 'android.test.mock' - - defaultConfig { - minSdkVersion 23 - targetSdkVersion 34 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - - testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = true - unitTests.all { - exclude '**/*TestSuite*' - } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - packagingOptions { - jniLibs { - pickFirsts += ['**/*.so'] - } - } - -} - -tasks.named("dokkaHtml") { - outputDirectory.set(file("$buildDir/html")) -} - -tasks.named("dokkaJavadoc") { - outputDirectory.set(file("$buildDir/javadoc")) -} - -/** - * JCenter Dependency Manager - */ -task sourcesJar(type: Jar) { - archiveClassifier.set('sources') - from android.sourceSets.main.java.srcDirs -} - -task javadocJar(type: Jar, dependsOn: dokkaHtml) { - archiveClassifier.set('javadoc') - from new File("$buildDir/generated-javadoc") -} - -artifacts { - archives sourcesJar - archives javadocJar -} - -apply from: '../config/logger.gradle' -apply from: '../config/publish.gradle' - - -/** - * Dependencies - */ -dependencies { - api project(':forgerock-core') - implementation fileTree(dir: 'libs', include: ['*.jar']) - - // JWT - implementation 'com.nimbusds:nimbus-jose-jwt:9.25' - - // Common - implementation 'androidx.annotation:annotation:1.3.0' - implementation 'androidx.appcompat:appcompat:1.6.1' - - // FCM Notifications, make it optional for developer - compileOnly 'com.google.firebase:firebase-messaging:23.1.2' - - // Networking - implementation 'com.squareup.okhttp3:okhttp:4.11.0' - implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0' - - // Biometric - implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05' - - - // Testing - testImplementation 'androidx.test:core:1.5.0' - testImplementation 'androidx.test.ext:junit:1.1.5' - testImplementation 'androidx.test:runner:1.5.2' - testImplementation 'junit:junit:4.13.2' - testImplementation 'org.robolectric:robolectric:4.8.1' - testImplementation 'com.squareup.okhttp3:mockwebserver:4.8.0' - testImplementation "com.google.firebase:firebase-messaging:23.1.2" - testImplementation 'org.mockito:mockito-core:4.8.1' - - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - -} - diff --git a/forgerock-authenticator/build.gradle.kts b/forgerock-authenticator/build.gradle.kts new file mode 100644 index 00000000..11b92f52 --- /dev/null +++ b/forgerock-authenticator/build.gradle.kts @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2020 - 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +plugins { + id("com.android.library") + id("com.adarshr.test-logger") + id("maven-publish") + id("signing") + id("kotlin-android") +// We cannot use kdoc for this project due to Lombak ,so need to add this dokka plugin here. + id("org.jetbrains.dokka") +} + +apply() + +android { + namespace = "org.forgerock.android.authenticator" +} + +tasks.dokkaHtml.configure { + outputDirectory.set(file("$buildDir/html")) +} + +tasks.dokkaJavadoc.configure { + outputDirectory.set(file("$buildDir/javadoc")) +} + +/** + * JCenter Dependency Manager + */ +tasks { + val sourcesJar by creating(Jar::class) { + archiveClassifier.set("sources") + from(android.sourceSets.getByName("main").java.srcDirs) + } + + val javadocJar by creating(Jar::class) { + dependsOn.add(dokkaHtml) + archiveClassifier.set("javadoc") + from(File("$buildDir/generated-javadoc")) + } + + artifacts { + archives(sourcesJar) + archives(javadocJar) + } +} + +apply("../config/logger.gradle") +apply("../config/publish.gradle") + + +/** + * Dependencies + */ +dependencies { + api(project(":forgerock-core")) + + // JWT + implementation(libs.nimbus.jose.jwt) + + // Common + implementation(libs.androidx.annotation) + implementation(libs.androidx.appcompat) + + // FCM Notifications, make it optional for developer + compileOnly(libs.firebase.messaging) + + // Networking + implementation(libs.okhttp) + implementation(libs.logging.interceptor) + + // Biometric + implementation(libs.androidx.biometric.ktx) + + + // Testing + testImplementation(libs.androidx.test.core) + testImplementation(libs.androidx.test.ext.junit) + testImplementation(libs.androidx.test.runner) + testImplementation(libs.junit) + testImplementation(libs.org.robolectric.robolectric) + testImplementation(libs.okhttp3.mockwebserver) + testImplementation(libs.firebase.messaging) + testImplementation(libs.mockito.core) + + androidTestImplementation(libs.androidx.test.ext.junit) + +} + diff --git a/forgerock-authenticator/proguard-rules.pro b/forgerock-authenticator/proguard-rules.pro index f1b42451..2f9dc5a4 100644 --- a/forgerock-authenticator/proguard-rules.pro +++ b/forgerock-authenticator/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/forgerock-core/build.gradle b/forgerock-core/build.gradle deleted file mode 100644 index eff3042b..00000000 --- a/forgerock-core/build.gradle +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (c) 2020 - 2023 ForgeRock. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -apply plugin: 'com.android.library' -apply plugin: "com.adarshr.test-logger" -apply plugin: 'maven-publish' -apply plugin: 'signing' -apply plugin: 'kotlin-android' - -android { - namespace 'org.forgerock.android.core' - - compileSdk 34 - - useLibrary 'android.test.base' - useLibrary 'android.test.mock' - - defaultConfig { - minSdkVersion 23 - targetSdkVersion 34 - buildConfigField 'String', 'VERSION_NAME', "\"" + VERSION + "\"" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles 'consumer-rules.pro' - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - - testOptions { - unitTests.includeAndroidResources = true - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - packagingOptions { - jniLibs { - pickFirsts += ['**/*.so'] - } - } - - testOptions { - unitTests.all { - exclude '**/*TestSuite*' - } - } - - kotlinOptions { - freeCompilerArgs = ['-Xjvm-default=all'] - } -} - -apply from: '../config/logger.gradle' -apply from: '../config/kdoc.gradle' -apply from: '../config/publish.gradle' - -/** - * Dependencies - */ -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - - implementation 'com.squareup.okhttp3:okhttp:4.11.0' - implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0' - implementation 'androidx.annotation:annotation:1.6.0' - implementation 'androidx.security:security-crypto:1.1.0-alpha06' - - // Biometric - implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05' - - //Application Pin - compileOnly 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0' - - testImplementation 'androidx.test:core:1.5.0' - testImplementation 'androidx.test:runner:1.5.2' - - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation 'com.squareup.okhttp:mockwebserver:2.7.5' - androidTestImplementation 'commons-io:commons-io:2.6' - androidTestImplementation 'androidx.test:rules:1.5.0' - //Do not update to the latest library, Only 2.x compatible with Android M and below. - androidTestImplementation 'org.assertj:assertj-core:2.9.1' - - //For Application Pin - androidTestImplementation 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0' - androidTestImplementation 'androidx.security:security-crypto:1.1.0-alpha06' - - testImplementation 'androidx.test.ext:junit:1.1.5' - testImplementation 'junit:junit:4.13.2' - testImplementation 'org.robolectric:robolectric:4.9.2' - testImplementation 'com.squareup.okhttp:mockwebserver:2.7.5' - testImplementation 'commons-io:commons-io:2.6' - testImplementation 'org.assertj:assertj-core:3.23.1' - - testImplementation 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0' - testImplementation 'org.mockito:mockito-core:4.8.1' - testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0' - testImplementation 'org.powermock:powermock-module-junit4:2.0.9' - testImplementation 'org.powermock:powermock-api-mockito2:2.0.9' - - compileOnly "org.projectlombok:lombok:1.18.28" - delombok "org.projectlombok:lombok:1.18.28" - annotationProcessor 'org.projectlombok:lombok:1.18.28' -} - -repositories { - mavenCentral() -} \ No newline at end of file diff --git a/forgerock-core/build.gradle.kts b/forgerock-core/build.gradle.kts new file mode 100644 index 00000000..f6f8e83e --- /dev/null +++ b/forgerock-core/build.gradle.kts @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +plugins { + id("com.android.library") + id("com.adarshr.test-logger") + id("maven-publish") + id("signing") + id("kotlin-android") +} + +apply() + +android { + namespace = "org.forgerock.android.core" + + /** + * Comment this to debug instrument Test, + * There is an issue for AS to run NDK with instrument Test + */ + externalNativeBuild { + ndkBuild { + path("src/main/jni/Android.mk") + } + } + + val VERSION: String by project + defaultConfig { + buildConfigField("String", "VERSION_NAME", "\"$VERSION\"") + } + + packaging { + jniLibs { + pickFirsts.add("**/*.so") + } + } + + kotlinOptions { + freeCompilerArgs = listOf("-Xjvm-default=all") + } + +} + +apply("../config/logger.gradle") +apply("../config/kdoc.gradle") +apply("../config/publish.gradle") + +val delombok by configurations.creating { + extendsFrom(configurations.compileOnly.get()) +} + +/** + * Dependencies + */ +dependencies { + + implementation(libs.okhttp) + implementation(libs.logging.interceptor) + implementation(libs.androidx.annotation) + implementation(libs.androidx.security.crypto) + + // Biometric + implementation(libs.androidx.biometric.ktx) + + //Application Pin + compileOnly(libs.bcpkix.jdk15on) + + testImplementation(libs.androidx.test.core) + testImplementation(libs.androidx.test.runner) + + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.espresso.core) + androidTestImplementation(libs.mockwebserver) + androidTestImplementation(libs.commons.io) + androidTestImplementation(libs.rules) + //Do not update to the latest library, Only 2.x compatible with Android M and below. + androidTestImplementation(libs.assertj.core) + + //For Application Pin + androidTestImplementation(libs.bcpkix.jdk15on) + androidTestImplementation(libs.androidx.security.crypto) + + testImplementation(libs.androidx.test.ext.junit) + testImplementation(libs.junit) + testImplementation(libs.org.robolectric.robolectric) + testImplementation(libs.mockwebserver) + testImplementation(libs.commons.io) + testImplementation(libs.assertj.core) + + testImplementation(libs.bcpkix.jdk15on) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.powermock.module.junit4) + testImplementation(libs.powermock.api.mockito2) + + compileOnly(libs.projectlombok.lombok) + delombok(libs.projectlombok.lombok) + annotationProcessor(libs.projectlombok.lombok) +} \ No newline at end of file diff --git a/forgerock-core/proguard-rules.pro b/forgerock-core/proguard-rules.pro index f1b42451..2f9dc5a4 100644 --- a/forgerock-core/proguard-rules.pro +++ b/forgerock-core/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/forgerock-core/src/androidTest/java/org/forgerock/android/auth/AndroidVersionAwareTestRunner.java b/forgerock-core/src/androidTest/java/org/forgerock/android/auth/AndroidVersionAwareTestRunner.java deleted file mode 100644 index 875a35c7..00000000 --- a/forgerock-core/src/androidTest/java/org/forgerock/android/auth/AndroidVersionAwareTestRunner.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2019 ForgeRock. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -package org.forgerock.android.auth; - -import android.os.Build; -import org.junit.runner.notification.RunNotifier; -import org.junit.runners.BlockJUnit4ClassRunner; -import org.junit.runners.model.FrameworkMethod; -import org.junit.runners.model.InitializationError; - -public class AndroidVersionAwareTestRunner extends BlockJUnit4ClassRunner { - public AndroidVersionAwareTestRunner(Class klass) throws InitializationError { - super(klass); - } - - @Override - public void runChild(FrameworkMethod method, RunNotifier notifier) { - TargetApi condition = method.getAnnotation(TargetApi.class); - if(condition == null || Build.VERSION.SDK_INT >= condition.value()) { - super.runChild(method, notifier); - } else { - notifier.fireTestIgnored(describeChild(method)); - } - } -} diff --git a/forgerock-core/src/androidTest/java/org/forgerock/android/auth/TargetApi.java b/forgerock-core/src/androidTest/java/org/forgerock/android/auth/TargetApi.java deleted file mode 100644 index 4c632c97..00000000 --- a/forgerock-core/src/androidTest/java/org/forgerock/android/auth/TargetApi.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (c) 2019 ForgeRock. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -package org.forgerock.android.auth; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target( ElementType.METHOD ) -@Retention( RetentionPolicy.RUNTIME) -public @interface TargetApi { - int value(); -} diff --git a/forgerock-core/src/main/java/org/forgerock/android/auth/NetworkConfig.java b/forgerock-core/src/main/java/org/forgerock/android/auth/NetworkConfig.java index a0bcb042..3de5c698 100644 --- a/forgerock-core/src/main/java/org/forgerock/android/auth/NetworkConfig.java +++ b/forgerock-core/src/main/java/org/forgerock/android/auth/NetworkConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2020 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -7,25 +7,24 @@ package org.forgerock.android.auth; +import static java.util.concurrent.TimeUnit.SECONDS; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.concurrent.TimeUnit; -import lombok.Builder; -import lombok.Getter; import lombok.Singular; import okhttp3.CookieJar; import okhttp3.Interceptor; import okhttp3.OkHttpClient; -import static java.util.concurrent.TimeUnit.SECONDS; - -import androidx.annotation.NonNull; - /** * Manages Network configuration information */ -@Getter -class NetworkConfig { +public class NetworkConfig { private String identifier; @@ -43,7 +42,6 @@ class NetworkConfig { private List> buildSteps; - @Builder(builderMethodName = "networkBuilder") NetworkConfig(String identifier, @NonNull String host, Integer timeout, @@ -63,6 +61,10 @@ class NetworkConfig { this.buildSteps = buildSteps; } + public static NetworkConfigBuilder networkBuilder() { + return new NetworkConfigBuilder(); + } + CookieJar getCookieJar() { if (cookieJarSupplier != null) { return cookieJarSupplier.get(); @@ -84,4 +86,151 @@ String getIdentifier() { return identifier; } } + + public String getHost() { + return this.host; + } + + public Integer getTimeout() { + return this.timeout; + } + + public TimeUnit getTimeUnit() { + return this.timeUnit; + } + + public List getPins() { + return this.pins; + } + + public Supplier getCookieJarSupplier() { + return this.cookieJarSupplier; + } + + public Supplier> getInterceptorSupplier() { + return this.interceptorSupplier; + } + + public List> getBuildSteps() { + return this.buildSteps; + } + + public static class NetworkConfigBuilder { + private String identifier; + private String host; + private Integer timeout; + private TimeUnit timeUnit; + private Supplier cookieJarSupplier; + private ArrayList pins; + private Supplier> interceptorSupplier; + private ArrayList> buildSteps; + + NetworkConfigBuilder() { + } + + public NetworkConfigBuilder identifier(String identifier) { + this.identifier = identifier; + return this; + } + + public NetworkConfigBuilder host(String host) { + this.host = host; + return this; + } + + public NetworkConfigBuilder timeout(Integer timeout) { + this.timeout = timeout; + return this; + } + + public NetworkConfigBuilder timeUnit(TimeUnit timeUnit) { + this.timeUnit = timeUnit; + return this; + } + + public NetworkConfigBuilder cookieJarSupplier(Supplier cookieJarSupplier) { + this.cookieJarSupplier = cookieJarSupplier; + return this; + } + + public NetworkConfigBuilder pin(String pin) { + if (this.pins == null) this.pins = new ArrayList(); + this.pins.add(pin); + return this; + } + + public NetworkConfigBuilder pins(Collection pins) { + if (pins == null) { + throw new NullPointerException("pins cannot be null"); + } + if (this.pins == null) this.pins = new ArrayList(); + this.pins.addAll(pins); + return this; + } + + public NetworkConfigBuilder clearPins() { + if (this.pins != null) + this.pins.clear(); + return this; + } + + public NetworkConfigBuilder interceptorSupplier(Supplier> interceptorSupplier) { + this.interceptorSupplier = interceptorSupplier; + return this; + } + + public NetworkConfigBuilder buildStep(BuildStep buildStep) { + if (this.buildSteps == null) + this.buildSteps = new ArrayList<>(); + this.buildSteps.add(buildStep); + return this; + } + + public NetworkConfigBuilder buildSteps(Collection> buildSteps) { + if (buildSteps == null) { + throw new NullPointerException("buildSteps cannot be null"); + } + if (this.buildSteps == null) + this.buildSteps = new ArrayList>(); + this.buildSteps.addAll(buildSteps); + return this; + } + + public NetworkConfigBuilder clearBuildSteps() { + if (this.buildSteps != null) + this.buildSteps.clear(); + return this; + } + + public NetworkConfig build() { + List pins; + switch (this.pins == null ? 0 : this.pins.size()) { + case 0: + pins = java.util.Collections.emptyList(); + break; + case 1: + pins = java.util.Collections.singletonList(this.pins.get(0)); + break; + default: + pins = List.copyOf(this.pins); + } + List> buildSteps; + switch (this.buildSteps == null ? 0 : this.buildSteps.size()) { + case 0: + buildSteps = java.util.Collections.emptyList(); + break; + case 1: + buildSteps = java.util.Collections.singletonList(this.buildSteps.get(0)); + break; + default: + buildSteps = List.copyOf(this.buildSteps); + } + + return new NetworkConfig(this.identifier, this.host, this.timeout, this.timeUnit, this.cookieJarSupplier, pins, this.interceptorSupplier, buildSteps); + } + + public String toString() { + return "NetworkConfig.NetworkConfigBuilder(identifier=" + this.identifier + ", host=" + this.host + ", timeout=" + this.timeout + ", timeUnit=" + this.timeUnit + ", cookieJarSupplier=" + this.cookieJarSupplier + ", pins=" + this.pins + ", interceptorSupplier=" + this.interceptorSupplier + ", buildSteps=" + this.buildSteps + ")"; + } + } } diff --git a/forgerock-core/src/main/java/org/forgerock/android/auth/OkHttpClientProvider.java b/forgerock-core/src/main/java/org/forgerock/android/auth/OkHttpClientProvider.java deleted file mode 100644 index 4444920c..00000000 --- a/forgerock-core/src/main/java/org/forgerock/android/auth/OkHttpClientProvider.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2019 - 2022 ForgeRock. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -package org.forgerock.android.auth; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import okhttp3.CertificatePinner; -import okhttp3.CookieJar; -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.logging.HttpLoggingInterceptor; - -/** - * Provider to Cache and provide OKHttpClient - */ -class OkHttpClientProvider { - - private static final OkHttpClientProvider INSTANCE = new OkHttpClientProvider(); - - private Map cache = new ConcurrentHashMap<>(); - - private final InterceptorProvider interceptorProvider = new InterceptorProvider(); - - private OkHttpClientProvider() { - CoreEventDispatcher.CLEAR_OKHTTP.addObserver((o, arg) -> clear()); - } - - public static OkHttpClientProvider getInstance() { - return INSTANCE; - } - - /** - * Create or lookup a cached OKHttpClient - * - * @param networkConfig The Server configuration - * @return The OkHttpClient - */ - OkHttpClient lookup(NetworkConfig networkConfig) { - OkHttpClient client = cache.get(networkConfig.getIdentifier()); - - if (client != null) { - return client; - } - - OkHttpClient.Builder builder = new OkHttpClient.Builder() - .connectTimeout(networkConfig.getTimeout(), networkConfig.getTimeUnit()) - .readTimeout(networkConfig.getTimeout(), networkConfig.getTimeUnit()) - .writeTimeout(networkConfig.getTimeout(), networkConfig.getTimeUnit()) - .followRedirects(false); - - if (networkConfig.getCookieJar() == null) { - builder.cookieJar(CookieJar.NO_COOKIES); - } else { - builder.cookieJar(networkConfig.getCookieJar()); - } - - if (networkConfig.getInterceptorSupplier() != null && - networkConfig.getInterceptorSupplier().get() != null) { - for (Interceptor i : networkConfig.getInterceptorSupplier().get()) { - builder.addInterceptor(i); - } - } - - HttpLoggingInterceptor networkInterceptor = interceptorProvider.getInterceptor(); - if (networkInterceptor != null) { - builder.addInterceptor(networkInterceptor); - } - - if (!networkConfig.getPins().isEmpty()) { - CertificatePinner.Builder cpBuilder = new CertificatePinner.Builder(); - for (String s : networkConfig.getPins()) { - cpBuilder.add(networkConfig.getHost(), "sha256/" + s); - } - builder.certificatePinner(cpBuilder.build()); - } - - for (BuildStep buildStep : networkConfig.getBuildSteps()) { - if (buildStep != null) { - buildStep.build(builder); - } - } - - client = builder.build(); - cache.put(networkConfig.getIdentifier(), client); - return client; - - } - - /** - * Clear the cached {{@link OkHttpClient}} - */ - public void clear() { - cache.clear(); - } -} diff --git a/forgerock-core/src/main/java/org/forgerock/android/auth/OkHttpClientProvider.kt b/forgerock-core/src/main/java/org/forgerock/android/auth/OkHttpClientProvider.kt new file mode 100644 index 00000000..7932a5a2 --- /dev/null +++ b/forgerock-core/src/main/java/org/forgerock/android/auth/OkHttpClientProvider.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth + +import okhttp3.CertificatePinner +import okhttp3.CookieJar +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import java.util.concurrent.ConcurrentHashMap + +/** + * Provider to Cache and provide OKHttpClient + */ +class OkHttpClientProvider private constructor() { + private val cache: MutableMap = ConcurrentHashMap() + private val interceptorProvider = InterceptorProvider() + + init { + CoreEventDispatcher.CLEAR_OKHTTP.addObserver { _, _ -> clear() } + } + + /** + * Create or lookup a cached OKHttpClient + * + * @param networkConfig The Server configuration + * @return The OkHttpClient + */ + fun lookup(networkConfig: NetworkConfig): OkHttpClient { + var client = cache[networkConfig.identifier] + if (client != null) { + return client + } + val builder = + OkHttpClient.Builder() + .connectTimeout(networkConfig.timeout.toLong(), networkConfig.timeUnit) + .readTimeout(networkConfig.timeout.toLong(), networkConfig.timeUnit) + .writeTimeout(networkConfig.timeout.toLong(), networkConfig.timeUnit) + .followRedirects(false) + if (networkConfig.cookieJar == null) { + builder.cookieJar(CookieJar.NO_COOKIES) + } else { + builder.cookieJar(networkConfig.cookieJar) + } + if (networkConfig.interceptorSupplier != null && + networkConfig.interceptorSupplier.get() != null + ) { + for (i in networkConfig.interceptorSupplier.get()) { + builder.addInterceptor(i) + } + } + + builder.addInterceptor( + Interceptor { chain -> + val request = + chain.request().newBuilder() + .header(REQUESTED_WITH_KEY, REQUESTED_WITH_VALUE) + .header(REQUESTED_PLATFORM_KEY, REQUESTED_PLATFORM_VALUE) + .build() + return@Interceptor chain.proceed(request) + }, + ) + + val networkInterceptor: HttpLoggingInterceptor? = interceptorProvider.getInterceptor() + networkInterceptor?.let { + builder.addInterceptor(it) + } + + if (networkConfig.pins.isNotEmpty()) { + val cpBuilder = CertificatePinner.Builder() + for (s in networkConfig.pins) { + cpBuilder.add(networkConfig.host, "sha256/$s") + } + builder.certificatePinner(cpBuilder.build()) + } + for (buildStep in networkConfig.buildSteps) { + buildStep?.build(builder) + } + + client = builder.build() + cache[networkConfig.identifier] = client + return client + } + + /** + * Clear the cached {[OkHttpClient]} + */ + fun clear() { + cache.clear() + } + + companion object { + private val providerInstance = OkHttpClientProvider() + private const val REQUESTED_WITH_KEY = "x-requested-with" + private const val REQUESTED_WITH_VALUE = "forgerock-sdk" + private const val REQUESTED_PLATFORM_KEY = "x-requested-platform" + private const val REQUESTED_PLATFORM_VALUE = "android" + + @JvmStatic + fun getInstance(): OkHttpClientProvider { + return providerInstance + } + } +} diff --git a/forgerock-auth/src/main/jni/Android.mk b/forgerock-core/src/main/jni/Android.mk similarity index 100% rename from forgerock-auth/src/main/jni/Android.mk rename to forgerock-core/src/main/jni/Android.mk diff --git a/forgerock-auth/src/main/jni/Application.mk b/forgerock-core/src/main/jni/Application.mk similarity index 100% rename from forgerock-auth/src/main/jni/Application.mk rename to forgerock-core/src/main/jni/Application.mk diff --git a/forgerock-auth/src/main/jni/toolFile.cpp b/forgerock-core/src/main/jni/toolFile.cpp similarity index 100% rename from forgerock-auth/src/main/jni/toolFile.cpp rename to forgerock-core/src/main/jni/toolFile.cpp diff --git a/forgerock-core/src/test/java/org/forgerock/android/auth/RequestInterceptorTest.java b/forgerock-core/src/test/java/org/forgerock/android/auth/RequestInterceptorTest.java index 5a80bdad..d09efd48 100644 --- a/forgerock-core/src/test/java/org/forgerock/android/auth/RequestInterceptorTest.java +++ b/forgerock-core/src/test/java/org/forgerock/android/auth/RequestInterceptorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2020 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -62,14 +62,14 @@ public void setUp() throws IOException, JSONException { data = new JSONObject(); data.put("test", "test"); OkHttpClientProvider.getInstance().clear(); - RequestInterceptorRegistry.getInstance().register(null); + RequestInterceptorRegistry.getInstance().register((RequestInterceptor) null); } @After public void tearDown() throws Exception { server.shutdown(); OkHttpClientProvider.getInstance().clear(); - RequestInterceptorRegistry.getInstance().register(null); + RequestInterceptorRegistry.getInstance().register((RequestInterceptor) null); } private String getUrl() { @@ -128,6 +128,8 @@ public void testChainIntercept() throws InterruptedException { RecordedRequest recordedRequest = server.takeRequest(); assertThat(recordedRequest.getHeader("HeaderName")).isEqualTo("HeaderValue"); assertThat(recordedRequest.getHeader("HeaderName2")).isEqualTo("HeaderValue2"); + assertThat(recordedRequest.getHeader("x-requested-with")).isEqualTo("forgerock-sdk"); + assertThat(recordedRequest.getHeader("x-requested-platform")).isEqualTo("android"); } @Test @@ -144,7 +146,10 @@ public void testAddHeader() throws InterruptedException { .get() .build(); send(networkConfig, request); - assertThat(server.takeRequest().getHeader("HeaderName")).isEqualTo("HeaderValue"); + RecordedRequest recordedRequest = server.takeRequest(); + assertThat(recordedRequest.getHeader("HeaderName")).isEqualTo("HeaderValue"); + assertThat(recordedRequest.getHeader("x-requested-with")).isEqualTo("forgerock-sdk"); + assertThat(recordedRequest.getHeader("x-requested-platform")).isEqualTo("android"); } @Test @@ -161,7 +166,10 @@ public void testRemoveHeader() throws InterruptedException { .get() .build(); send(networkConfig, request); - assertThat(server.takeRequest().getHeader("HeaderName")).isNull(); + RecordedRequest recordedRequest = server.takeRequest(); + assertThat(recordedRequest.getHeader("HeaderName")).isNull(); + assertThat(recordedRequest.getHeader("x-requested-with")).isEqualTo("forgerock-sdk"); + assertThat(recordedRequest.getHeader("x-requested-platform")).isEqualTo("android"); } @Test @@ -178,7 +186,12 @@ public void testReplaceHeader() throws InterruptedException { .get() .build(); send(networkConfig, request); - assertThat(server.takeRequest().getHeader("HeaderName")).isEqualTo("HeaderValue2"); + + RecordedRequest recordedRequest = server.takeRequest(); + + assertThat(recordedRequest.getHeader("HeaderName")).isEqualTo("HeaderValue2"); + assertThat(recordedRequest.getHeader("x-requested-with")).isEqualTo("forgerock-sdk"); + assertThat(recordedRequest.getHeader("x-requested-platform")).isEqualTo("android"); } @Test @@ -194,7 +207,10 @@ public void testCustomizeUrl() throws InterruptedException { .get() .build(); send(networkConfig, request); - assertThat(server.takeRequest().getPath()).isEqualTo("/somewhere"); + RecordedRequest recordedRequest = server.takeRequest(); + assertThat(recordedRequest.getPath()).isEqualTo("/somewhere"); + assertThat(recordedRequest.getHeader("x-requested-with")).isEqualTo("forgerock-sdk"); + assertThat(recordedRequest.getHeader("x-requested-platform")).isEqualTo("android"); } @@ -229,9 +245,12 @@ public void testValidateClearHTTPEventFired() throws InterruptedException { @Test public void testCustomizeParam() throws InterruptedException, JSONException { - RequestInterceptorRegistry.getInstance().register( - request -> request.newBuilder().url(getUrl() + "?forceAuth=true").build()); + request -> request.newBuilder().url(getUrl() + "?forceAuth=true") + .header("custom_param", "custom-value") + .header("x-requested-with", "jey") + .header("x-requested-platform", "andy") + .build()); NetworkConfig networkConfig = NetworkConfig.networkBuilder() .host(server.getHostName()) @@ -241,8 +260,11 @@ public void testCustomizeParam() throws InterruptedException, JSONException { .get() .build(); send(networkConfig, request); - Uri uri = Uri.parse(server.takeRequest().getPath()); + RecordedRequest recordedRequest = server.takeRequest(); + Uri uri = Uri.parse(recordedRequest.getPath()); assertThat(uri.getQueryParameter("forceAuth")).isEqualTo("true"); + assertThat(recordedRequest.getHeader("x-requested-with")).isEqualTo("forgerock-sdk"); + assertThat(recordedRequest.getHeader("x-requested-platform")).isEqualTo("android"); } @@ -315,6 +337,8 @@ public void testPut() throws InterruptedException, JSONException { JSONObject result = new JSONObject(recordedRequest.getBody().readUtf8()); assertThat(result.getString("sampleName")).isEqualTo("sampleValue"); assertThat(recordedRequest.getMethod()).isEqualTo("PUT"); + assertThat(recordedRequest.getHeader("x-requested-with")).isEqualTo("forgerock-sdk"); + assertThat(recordedRequest.getHeader("x-requested-platform")).isEqualTo("android"); } @@ -340,6 +364,8 @@ public void testPatch() throws JSONException, InterruptedException { JSONObject result = new JSONObject(recordedRequest.getBody().readUtf8()); assertThat(result.getString("sampleName")).isEqualTo("sampleValue"); assertThat(recordedRequest.getMethod()).isEqualTo("PATCH"); + assertThat(recordedRequest.getHeader("x-requested-with")).isEqualTo("forgerock-sdk"); + assertThat(recordedRequest.getHeader("x-requested-platform")).isEqualTo("android"); } @@ -363,6 +389,8 @@ public void testDelete() throws JSONException, InterruptedException { RecordedRequest recordedRequest = server.takeRequest(); assertThat(recordedRequest.getBody().size()).isEqualTo(0); assertThat(recordedRequest.getMethod()).isEqualTo("DELETE"); + assertThat(recordedRequest.getHeader("x-requested-with")).isEqualTo("forgerock-sdk"); + assertThat(recordedRequest.getHeader("x-requested-platform")).isEqualTo("android"); } @Test @@ -387,6 +415,8 @@ public void testDeleteWithBody() throws JSONException, InterruptedException { JSONObject result = new JSONObject(recordedRequest.getBody().readUtf8()); assertThat(result.getString("sampleName")).isEqualTo("sampleValue"); assertThat(recordedRequest.getMethod()).isEqualTo("DELETE"); + assertThat(recordedRequest.getHeader("x-requested-with")).isEqualTo("forgerock-sdk"); + assertThat(recordedRequest.getHeader("x-requested-platform")).isEqualTo("android"); } @@ -463,6 +493,8 @@ public void testCookieIntercept() throws InterruptedException { send(networkConfig, request); RecordedRequest recordedRequest = server.takeRequest(); assertThat(recordedRequest.getHeader("Cookie")).isEqualTo("test=testValue"); + assertThat(recordedRequest.getHeader("x-requested-with")).isEqualTo("forgerock-sdk"); + assertThat(recordedRequest.getHeader("x-requested-platform")).isEqualTo("android"); } private interface CustomCookieJar extends CookieJar, OkHttpCookieInterceptor { diff --git a/forgerock-integration-tests/.gitignore b/forgerock-integration-tests/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/forgerock-integration-tests/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/forgerock-integration-tests/build.gradle.kts b/forgerock-integration-tests/build.gradle.kts new file mode 100644 index 00000000..d6a39968 --- /dev/null +++ b/forgerock-integration-tests/build.gradle.kts @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + alias(libs.plugins.androidLibrary) + alias(libs.plugins.kotlinAndroid) +} + +apply() + +android { + + namespace = "org.forgerock.android.integration" + testNamespace = "org.forgerock.android.integration.test" +} + +dependencies { + api(project(":forgerock-authenticator")) + api(project(":forgerock-auth")) + api(project(":ping-protect")) + + implementation(libs.appcompat) + implementation(libs.material) + implementation(libs.firebase.crashlytics.buildtools) + testImplementation(libs.junit) + + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.espresso.core) + androidTestImplementation(libs.mockwebserver) + androidTestImplementation(libs.commons.io) + androidTestImplementation(libs.rules) + androidTestImplementation(libs.play.services.location) + //Do not update to the latest library, Only 2.x compatible with Android M and below. + androidTestImplementation(libs.assertj.core) + androidTestImplementation(libs.play.services.fido) + + androidTestImplementation(libs.androidx.biometric.ktx) + androidTestImplementation(libs.nimbus.jose.jwt) + androidTestImplementation(libs.okhttp) + + //For Application Pin + androidTestImplementation(libs.bcpkix.jdk15on) + androidTestImplementation(libs.androidx.security.crypto) + + //App Integrity + androidTestImplementation(libs.integrity) +} \ No newline at end of file diff --git a/forgerock-integration-tests/consumer-rules.pro b/forgerock-integration-tests/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/forgerock-integration-tests/proguard-rules.pro b/forgerock-integration-tests/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/forgerock-integration-tests/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/forgerock-auth/src/androidTest/AndroidManifest.xml b/forgerock-integration-tests/src/androidTest/AndroidManifest.xml similarity index 100% rename from forgerock-auth/src/androidTest/AndroidManifest.xml rename to forgerock-integration-tests/src/androidTest/AndroidManifest.xml diff --git a/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/AndroidBaseTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/AndroidBaseTest.java new file mode 100644 index 00000000..a9e92766 --- /dev/null +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/AndroidBaseTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2020 - 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package org.forgerock.android.auth; + +import static org.assertj.core.api.Assertions.assertThat; + +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; + +import org.forgerock.android.auth.callback.Callback; +import org.forgerock.android.auth.callback.KbaCreateCallback; +import org.forgerock.android.auth.callback.NameCallback; +import org.forgerock.android.auth.callback.PasswordCallback; +import org.forgerock.android.auth.callback.StringAttributeInputCallback; +import org.forgerock.android.auth.callback.TermsAndConditionsCallback; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.Timeout; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +public abstract class AndroidBaseTest { + + @Rule + public Timeout timeout = new Timeout(10000, TimeUnit.MILLISECONDS); + protected static Context context = ApplicationProvider.getApplicationContext(); + public static String USERNAME = "sdkuser"; + public static String PASSWORD = "password"; + public static String USER_EMAIL = "sdkuser@example.com"; + + protected String TREE = "UsernamePassword"; + + @Before + public void setUpSDK() { + Logger.set(Logger.Level.DEBUG); + FRAuth.start(context); + } + + /** + * Register a random user + * + * @return The username of the registered user + * @throws ExecutionException + * @throws InterruptedException + */ + public static String registerRandomUser() throws ExecutionException, InterruptedException { + String randomUsername = "user" + System.currentTimeMillis(); + + NodeListenerFuture nodeListenerFuture = getNodeListenerFuture(randomUsername); + + FRSession.authenticate(context, "TEST_USER_REGISTRATION", nodeListenerFuture); + Assert.assertNotNull(nodeListenerFuture.get()); + Assert.assertNotNull(FRSession.getCurrentSession()); + Assert.assertNotNull(FRSession.getCurrentSession().getSessionToken()); + + return randomUsername; + } + + protected static NodeListenerFuture getNodeListenerFuture(String userName) { + return new UsernamePasswordNodeListener(context) { + @Override + public void onCallbackReceived(Node node) { + if (node.getCallback(NameCallback.class) != null) { + node.getCallback(NameCallback.class).setName(userName); + node.next(context, this); + } + if (node.getCallback(PasswordCallback.class) != null) { + PasswordCallback callback = node.getCallback(PasswordCallback.class); + assertThat(callback.getPrompt()).isEqualTo("Password"); + callback.setPassword(userName.toCharArray()); + node.next(context, this ); + } + if (node.getCallback(StringAttributeInputCallback.class) != null) { + List callbacks = node.getCallbacks(); + + StringAttributeInputCallback givenName = (StringAttributeInputCallback) callbacks.get(0); + StringAttributeInputCallback sn = (StringAttributeInputCallback) callbacks.get(1); + StringAttributeInputCallback mail = (StringAttributeInputCallback) callbacks.get(2); + + givenName.setValue(userName); + sn.setValue(userName); + mail.setValue(userName + "@example.com"); + + node.next(context, this ); + } + if (node.getCallback(KbaCreateCallback.class) != null) { + List callbacks = node.getCallbacks(); + + KbaCreateCallback firstQuestion = (KbaCreateCallback) callbacks.get(0); + firstQuestion.setSelectedQuestion(firstQuestion.getPredefinedQuestions().get(0)); + firstQuestion.setSelectedAnswer("Test"); + + // Uncomment this block if there are more than one KbaCreateCallbacks in the tree + /* + KbaCreateCallback secondQuestion = (KbaCreateCallback) callbacks.get(1); + secondQuestion.setSelectedQuestion(secondQuestion.getPredefinedQuestions().get(1)); + secondQuestion.setSelectedAnswer("Test"); + */ + + node.next(context, this ); + } + + if (node.getCallback(TermsAndConditionsCallback.class) != null) { + TermsAndConditionsCallback callback = node.getCallback(TermsAndConditionsCallback.class); + callback.setAccept(true); + + node.next(context, this ); + } + } + }; + } +} diff --git a/forgerock-core/src/androidTest/java/org/forgerock/android/auth/AndroidMSecuredSharedPreferences.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/AndroidMSecuredSharedPreferences.java similarity index 100% rename from forgerock-core/src/androidTest/java/org/forgerock/android/auth/AndroidMSecuredSharedPreferences.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/AndroidMSecuredSharedPreferences.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/AndroidVersionAwareTestRunner.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/AndroidVersionAwareTestRunner.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/AndroidVersionAwareTestRunner.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/AndroidVersionAwareTestRunner.java diff --git a/forgerock-core/src/androidTest/java/org/forgerock/android/auth/AppPinAuthenticatorTest.kt b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/AppPinAuthenticatorTest.kt similarity index 77% rename from forgerock-core/src/androidTest/java/org/forgerock/android/auth/AppPinAuthenticatorTest.kt rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/AppPinAuthenticatorTest.kt index ac65c68a..c9575edd 100644 --- a/forgerock-core/src/androidTest/java/org/forgerock/android/auth/AppPinAuthenticatorTest.kt +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/AppPinAuthenticatorTest.kt @@ -1,9 +1,18 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + package org.forgerock.android.auth import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import org.assertj.core.api.Assertions +import org.junit.Assert.fail +import org.junit.Assert.assertTrue import org.junit.After import org.junit.Before import org.junit.Test @@ -35,10 +44,19 @@ class AppPinAuthenticatorTest { Assertions.assertThat(privateKey).isNotNull } - @Test(expected = UnrecoverableKeyException::class) + @Test fun testInvalidPin() { appPinAuthenticator.generateKeys(context, "1234".toCharArray()) - appPinAuthenticator.getPrivateKey(context, "invalid".toCharArray()) + try { + appPinAuthenticator.getPrivateKey(context, "invalid".toCharArray()) + fail() + } + catch (e: UnrecoverableKeyException) { + assertTrue(true) + } + catch (e: IOException) { + assertTrue(true) + } } @Test(expected = IOException::class) diff --git a/forgerock-core/src/androidTest/java/org/forgerock/android/auth/CoreInstrumentTestSuite.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/CoreInstrumentTestSuite.java similarity index 100% rename from forgerock-core/src/androidTest/java/org/forgerock/android/auth/CoreInstrumentTestSuite.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/CoreInstrumentTestSuite.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/DefaultSingleSignOnManagerTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/DefaultSingleSignOnManagerTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/DefaultSingleSignOnManagerTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/DefaultSingleSignOnManagerTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/DefaultTokenManagerTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/DefaultTokenManagerTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/DefaultTokenManagerTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/DefaultTokenManagerTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/DummyActivity.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/DummyActivity.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/DummyActivity.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/DummyActivity.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/FRDeviceProfileTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/FRDeviceProfileTest.java similarity index 74% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/FRDeviceProfileTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/FRDeviceProfileTest.java index c26bc9d4..f65b6225 100644 --- a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/FRDeviceProfileTest.java +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/FRDeviceProfileTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 - 2022 ForgeRock. All rights reserved. + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -13,9 +13,10 @@ import android.Manifest; import android.content.Context; +import android.content.pm.PackageManager; +import android.location.LocationManager; import android.os.Build; -import androidx.annotation.RequiresApi; import androidx.core.app.ActivityCompat; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SdkSuppress; @@ -32,10 +33,12 @@ import org.junit.Test; import org.junit.runner.RunWith; +import java.util.List; import java.util.concurrent.ExecutionException; @RunWith(AndroidJUnit4.class) public class FRDeviceProfileTest extends AndroidBaseTest { + private static final String TAG = FRDeviceProfileTest.class.getSimpleName(); @Rule public GrantPermissionRule permissionRule = GrantPermissionRule.grant( @@ -44,6 +47,9 @@ public class FRDeviceProfileTest extends AndroidBaseTest { Manifest.permission.BLUETOOTH ); + @Rule + public SkipTestOnPermissionFailureRule skipRule = new SkipTestOnPermissionFailureRule(); + @Before public void setUp() throws Exception { Config.getInstance().init(context, null); @@ -52,6 +58,7 @@ public void setUp() throws Exception { @Test @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) public void testDeviceProfile() throws JSONException, ExecutionException, InterruptedException { + Logger.set(Logger.Level.DEBUG); FRListenerFuture future = new FRListenerFuture<>(); FRDevice.getInstance().getProfile(future); @@ -99,13 +106,35 @@ public void testDeviceProfile() throws JSONException, ExecutionException, Interr // Location may not be captured using emulator // or if ACCESS_BACKGROUND_LOCATION permission is not granted - int backgroundLocationPermissionApproved = - ActivityCompat.checkSelfPermission(context, - Manifest.permission.ACCESS_BACKGROUND_LOCATION); + boolean locationPermissionGranted = false; + + // If the location services are NOT enabled skip the rest of the test... + LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + if (locationManager == null) { + Logger.debug(TAG, "Location service is disabled. Skipping the rest of the test..."); + return; + } + + // If there are no providers enabled skip the rest of the test. Providers can be "network", "gps" or "passive"... + List providers = locationManager.getProviders(true); + if(providers.isEmpty()) { + Logger.debug(TAG, "No location providers are available. Skipping the rest of the test..."); + return; + } - if (!isEmulator() && backgroundLocationPermissionApproved >= 0) { - result.getJSONObject("location").getDouble("latitude"); - result.getJSONObject("location").getDouble("longitude"); + // If none of the location permissions are granted then location info is omitted... + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || + ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED || + ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED ) { + Logger.debug(TAG, "Location permissions are granted!..."); + locationPermissionGranted = true; + } + + if (!isEmulator() && locationPermissionGranted) { + Logger.debug(TAG, "Location data should exist!"); + Logger.debug(TAG, result.toString()); + assertTrue(result.getJSONObject("location").has("latitude")); + assertTrue(result.getJSONObject("location").has("longitude")); } } @@ -147,12 +176,8 @@ public void multipleCustomCollector() throws JSONException, ExecutionException, collector.collect(context, result); result.get().getJSONObject("bluetooth").getBoolean("supported"); result.get().getJSONObject("telephony").getString("networkCountryIso"); - } - - - private boolean isEmulator() { return Build.PRODUCT.matches(".*_?sdk_?.*"); } diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/FRSessionTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/FRSessionTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/FRSessionTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/FRSessionTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/FRUserTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/FRUserTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/FRUserTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/FRUserTest.java diff --git a/forgerock-authenticator/src/androidTest/java/org/forgerock/android/auth/MockModelBuilder.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/MockModelBuilder.java similarity index 100% rename from forgerock-authenticator/src/androidTest/java/org/forgerock/android/auth/MockModelBuilder.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/MockModelBuilder.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/MockServer.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/MockServer.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/MockServer.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/MockServer.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/PlatformUsernamePasswordNodeListener.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/PlatformUsernamePasswordNodeListener.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/PlatformUsernamePasswordNodeListener.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/PlatformUsernamePasswordNodeListener.java diff --git a/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/RetryTestRule.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/RetryTestRule.java new file mode 100644 index 00000000..0184a744 --- /dev/null +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/RetryTestRule.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package org.forgerock.android.auth; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +public class RetryTestRule implements TestRule { + private int retryCount; + + public RetryTestRule(int retryCount) { + this.retryCount = retryCount; + } + + public Statement apply(Statement base, Description description) { + return statement(base, description); + } + + private Statement statement(final Statement base, final Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + Throwable caughtThrowable = null; + + for (int i = 0; i < retryCount; i++) { + try { + base.evaluate(); + return; + } catch (Throwable t) { + caughtThrowable = t; + System.err.println(description.getDisplayName() + ": run " + (i + 1) + " failed"); + } + } + System.err.println(description.getDisplayName() + ": giving up after " + retryCount + " failures"); + throw caughtThrowable; + } + }; + } +} diff --git a/forgerock-core/src/androidTest/java/org/forgerock/android/auth/RootDeviceTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/RootDeviceTest.java similarity index 100% rename from forgerock-core/src/androidTest/java/org/forgerock/android/auth/RootDeviceTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/RootDeviceTest.java diff --git a/forgerock-core/src/androidTest/java/org/forgerock/android/auth/SecuredSharedPreferencesTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/SecuredSharedPreferencesTest.java similarity index 100% rename from forgerock-core/src/androidTest/java/org/forgerock/android/auth/SecuredSharedPreferencesTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/SecuredSharedPreferencesTest.java diff --git a/forgerock-authenticator/src/androidTest/java/org/forgerock/android/auth/SecuredStorageTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/SecuredStorageTest.java similarity index 100% rename from forgerock-authenticator/src/androidTest/java/org/forgerock/android/auth/SecuredStorageTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/SecuredStorageTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/SetPersistentCookieTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/SetPersistentCookieTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/SetPersistentCookieTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/SetPersistentCookieTest.java diff --git a/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/SkipTestOnPermissionFailureRule.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/SkipTestOnPermissionFailureRule.java new file mode 100644 index 00000000..e1c92712 --- /dev/null +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/SkipTestOnPermissionFailureRule.java @@ -0,0 +1,28 @@ +package org.forgerock.android.auth; + +import org.junit.AssumptionViolatedException; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import junit.framework.AssertionFailedError; + +public class SkipTestOnPermissionFailureRule implements TestRule { + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + try { + base.evaluate(); + } catch (AssertionFailedError e) { + if (e.getMessage().contains("Failed to grant permissions")) { + throw new AssumptionViolatedException("Skipping test due to failure to grant permissions"); + } else { + throw e; // Re-throw other AssertionFailedError + } + } + } + }; + } +} \ No newline at end of file diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/TargetApi.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/TargetApi.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/TargetApi.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/TargetApi.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/TreeTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/TreeTest.java similarity index 87% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/TreeTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/TreeTest.java index 8af4c91e..8e1d8afa 100644 --- a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/TreeTest.java +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/TreeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 ForgeRock. All rights reserved. + * Copyright (c) 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -9,8 +9,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.forgerock.android.auth.callback.NameCallback; -import org.forgerock.android.auth.callback.PasswordCallback; import org.junit.After; import org.junit.Assert; import org.junit.Test; diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/UsernamePasswordNodeListener.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/UsernamePasswordNodeListener.java similarity index 86% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/UsernamePasswordNodeListener.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/UsernamePasswordNodeListener.java index d92bf882..940e4c3f 100644 --- a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/UsernamePasswordNodeListener.java +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/UsernamePasswordNodeListener.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 ForgeRock. All rights reserved. + * Copyright (c) 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -7,17 +7,14 @@ package org.forgerock.android.auth; +import static org.forgerock.android.auth.AndroidBaseTest.PASSWORD; +import static org.forgerock.android.auth.AndroidBaseTest.USERNAME; + import android.content.Context; -import org.forgerock.android.auth.FRSession; -import org.forgerock.android.auth.Node; -import org.forgerock.android.auth.NodeListenerFuture; import org.forgerock.android.auth.callback.NameCallback; import org.forgerock.android.auth.callback.PasswordCallback; -import static org.forgerock.android.auth.AndroidBaseTest.PASSWORD; -import static org.forgerock.android.auth.AndroidBaseTest.USERNAME; - public class UsernamePasswordNodeListener extends NodeListenerFuture { private Context context; diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/AppIntegrityCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/AppIntegrityCallbackTest.java similarity index 99% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/AppIntegrityCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/AppIntegrityCallbackTest.java index 96711899..daee3649 100644 --- a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/AppIntegrityCallbackTest.java +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/AppIntegrityCallbackTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 - 2024 ForgeRock. All rights reserved. + * Copyright (c) 2022 - 2023 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/AppIntegrityNodeListener.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/AppIntegrityNodeListener.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/AppIntegrityNodeListener.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/AppIntegrityNodeListener.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/BaseDeviceBindingTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/BaseDeviceBindingTest.java similarity index 81% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/BaseDeviceBindingTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/BaseDeviceBindingTest.java index fff2642b..e7aae22a 100644 --- a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/BaseDeviceBindingTest.java +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/BaseDeviceBindingTest.java @@ -7,24 +7,29 @@ package org.forgerock.android.auth.callback; +import static org.assertj.core.api.Assertions.fail; import static org.forgerock.android.auth.devicebind.LocalDeviceBindingRepositoryKt.ORG_FORGEROCK_V_1_DEVICE_REPO; import android.content.Context; import androidx.test.core.app.ApplicationProvider; +import org.forgerock.android.auth.AndroidBaseTest; import org.forgerock.android.auth.EncryptedPreferences; import org.forgerock.android.auth.FRAuth; import org.forgerock.android.auth.FROptions; import org.forgerock.android.auth.FROptionsBuilder; import org.forgerock.android.auth.FRSession; import org.forgerock.android.auth.Logger; +import org.forgerock.android.auth.RetryTestRule; import org.junit.After; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.rules.ExpectedException; import org.junit.rules.Timeout; +import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; public abstract class BaseDeviceBindingTest { @@ -37,7 +42,7 @@ public abstract class BaseDeviceBindingTest { protected final static String OAUTH_REDIRECT_URI = "org.forgerock.demo:/oauth2redirect"; protected final static String SCOPE = "openid profile email address phone"; - protected final static String USERNAME = "sdkuser"; + protected static String USERNAME = null; protected static String KID = null; // Used to store the kid of the key generated during binding protected static String USER_ID = null; // Used to store the userId of the user who binds the device @@ -70,6 +75,15 @@ public static void setUpSDK() { FRAuth.start(context, options); // Clear all preexisting registered keys on the device EncryptedPreferences.Companion.getInstance(context, ORG_FORGEROCK_V_1_DEVICE_REPO).edit().clear().apply(); + + // Register a random user. This is needed to avoid test failure when tests are run in parallel on multiple devices + try { + USERNAME = AndroidBaseTest.registerRandomUser(); + } catch (ExecutionException e) { + fail("Failed to register a new random user: ", e); + } catch (InterruptedException e) { + fail("Failed to register a new random user: ", e); + } } @After diff --git a/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/BasePingOneProtectTest.kt b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/BasePingOneProtectTest.kt new file mode 100644 index 00000000..0cf75fad --- /dev/null +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/BasePingOneProtectTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth.callback + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import org.forgerock.android.auth.FRAuth +import org.forgerock.android.auth.FROptionsBuilder +import org.forgerock.android.auth.FRSession +import org.forgerock.android.auth.Logger +import org.forgerock.android.auth.Logger.Companion.set +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.rules.Timeout +import java.util.concurrent.TimeUnit + +abstract class BasePingOneProtectTest { + val context: Context = ApplicationProvider.getApplicationContext() + private val AM_URL = "https://openam-protect2.forgeblocks.com/am" + private val REALM = "alpha" + private val COOKIE_NAME = "c1c805de4c9b333" + private val OAUTH_CLIENT = "AndroidTest" + private val OAUTH_REDIRECT_URI = "org.forgerock.demo:/oauth2redirect" + private val SCOPE = "openid profile email address phone" + companion object { + var USERNAME = "sdkuser" + } + + private val options = FROptionsBuilder.build { + server { + url = AM_URL + realm = REALM + cookieName = COOKIE_NAME + } + oauth { + oauthClientId = OAUTH_CLIENT + oauthRedirectUri = OAUTH_REDIRECT_URI + oauthScope = SCOPE + } + } + + @Rule @JvmField + val timeout = Timeout(20000, TimeUnit.MILLISECONDS) + + @Before + fun setUpSDK() { + set(Logger.Level.DEBUG) + FRAuth.start(context, options) + } + + @After + fun logoutSession() { + FRSession.getCurrentSession()?.let { + it.logout() + } + } +} diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/BooleanAttributeInputCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/BooleanAttributeInputCallbackTest.java similarity index 94% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/BooleanAttributeInputCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/BooleanAttributeInputCallbackTest.java index 59df13dc..70fb13ef 100644 --- a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/BooleanAttributeInputCallbackTest.java +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/BooleanAttributeInputCallbackTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 ForgeRock. All rights reserved. + * Copyright (c) 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -7,19 +7,17 @@ package org.forgerock.android.auth.callback; -import org.assertj.core.api.Assertions; +import static org.assertj.core.api.Assertions.assertThat; + import org.forgerock.android.auth.FRSession; import org.forgerock.android.auth.Node; import org.forgerock.android.auth.NodeListenerFuture; import org.forgerock.android.auth.TreeTest; import org.forgerock.android.auth.UsernamePasswordNodeListener; -import org.json.JSONException; import java.util.List; import java.util.concurrent.ExecutionException; -import static org.assertj.core.api.Assertions.assertThat; - public class BooleanAttributeInputCallbackTest extends TreeTest { private int hit = 0; diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/ChoiceCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ChoiceCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/ChoiceCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ChoiceCallbackTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/ConfirmationCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ConfirmationCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/ConfirmationCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ConfirmationCallbackTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/ConsentMappingCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ConsentMappingCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/ConsentMappingCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ConsentMappingCallbackTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/CustomDeviceSigningVerifierCallback.kt b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/CustomDeviceSigningVerifierCallback.kt similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/CustomDeviceSigningVerifierCallback.kt rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/CustomDeviceSigningVerifierCallback.kt diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceBindingCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceBindingCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceBindingCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceBindingCallbackTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceBindingListAndUnbind.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceBindingListAndUnbind.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceBindingListAndUnbind.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceBindingListAndUnbind.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceBindingNodeListener.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceBindingNodeListener.java similarity index 94% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceBindingNodeListener.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceBindingNodeListener.java index 713a249c..ca8e5842 100644 --- a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceBindingNodeListener.java +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceBindingNodeListener.java @@ -7,7 +7,7 @@ package org.forgerock.android.auth.callback; -import static org.forgerock.android.auth.AndroidBaseTest.USERNAME; +import static org.forgerock.android.auth.callback.BaseDeviceBindingTest.USERNAME; import android.content.Context; diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceProfileCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceProfileCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceProfileCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceProfileCallbackTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceProfileCollectorCallbackAndroidTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceProfileCollectorCallbackAndroidTest.java similarity index 69% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceProfileCollectorCallbackAndroidTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceProfileCollectorCallbackAndroidTest.java index daccc1b0..942a79c3 100644 --- a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceProfileCollectorCallbackAndroidTest.java +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceProfileCollectorCallbackAndroidTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 - 2022 ForgeRock. All rights reserved. + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -12,6 +12,7 @@ import android.Manifest; import android.content.Context; +import android.content.pm.PackageManager; import android.location.LocationManager; import android.os.Build; @@ -22,13 +23,20 @@ import org.forgerock.android.auth.AndroidBaseTest; import org.forgerock.android.auth.FRListenerFuture; +import org.forgerock.android.auth.Log; +import org.forgerock.android.auth.Logger; +import org.forgerock.android.auth.RetryTestRule; +import org.forgerock.android.auth.SkipTestOnPermissionFailureRule; import org.json.JSONObject; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.List; + @RunWith(AndroidJUnit4.class) public class DeviceProfileCollectorCallbackAndroidTest extends AndroidBaseTest { + private static final String TAG = DeviceProfileCollectorCallbackAndroidTest.class.getSimpleName(); @Rule public GrantPermissionRule permissionRule = GrantPermissionRule.grant( @@ -37,6 +45,9 @@ public class DeviceProfileCollectorCallbackAndroidTest extends AndroidBaseTest { Manifest.permission.BLUETOOTH ); + @Rule + public SkipTestOnPermissionFailureRule skipRule = new SkipTestOnPermissionFailureRule(); + @Test public void testMetadata() throws Exception { @@ -78,6 +89,7 @@ public void testMetadata() throws Exception { @Test @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) public void testLocation() throws Exception { + Logger.set(Logger.Level.DEBUG); JSONObject raw = new JSONObject("{\n" + " \"type\": \"DeviceProfileCallback\",\n" + @@ -111,11 +123,33 @@ public void testLocation() throws Exception { assertTrue(content.contains("identifier")); - int backgroundLocationPermissionApproved = - ActivityCompat.checkSelfPermission(context, - Manifest.permission.ACCESS_BACKGROUND_LOCATION); + boolean locationPermissionGranted = false; + + + // If the location services are NOT enabled skip the rest of the test... + LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + if (locationManager == null) { + Logger.debug(TAG, "Location service is disabled. Skipping the rest of the test..."); + return; + } - if (!isEmulator() && backgroundLocationPermissionApproved >= 0) { + // If there are no providers enabled skip the rest of the test. Providers can be "network", "gps" or "passive"... + List providers = locationManager.getProviders(true); + if(providers.isEmpty()) { + Logger.debug(TAG, "No location providers are available. Skipping the rest of the test..."); + return; + } + + // If none of the location permissions are granted then location info is omitted... + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || + ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED || + ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED ) { + locationPermissionGranted = true; + } + + if (!isEmulator() && locationPermissionGranted) { + Logger.debug(TAG, "Location data should exist!"); + Logger.debug(TAG, content); assertTrue(content.contains("location")); } } @@ -123,5 +157,4 @@ public void testLocation() throws Exception { private boolean isEmulator() { return Build.PRODUCT.matches(".*_?sdk_?.*"); } - } \ No newline at end of file diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceSigningVerifierApplicationPinCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceSigningVerifierApplicationPinCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceSigningVerifierApplicationPinCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceSigningVerifierApplicationPinCallbackTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceSigningVerifierCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceSigningVerifierCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceSigningVerifierCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceSigningVerifierCallbackTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceSigningVerifierNodeListener.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceSigningVerifierNodeListener.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/DeviceSigningVerifierNodeListener.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/DeviceSigningVerifierNodeListener.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/HiddenValueCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/HiddenValueCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/HiddenValueCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/HiddenValueCallbackTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/KbaCreateCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/KbaCreateCallbackTest.java similarity index 94% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/KbaCreateCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/KbaCreateCallbackTest.java index 6a72f2fd..4e8b6dd0 100644 --- a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/KbaCreateCallbackTest.java +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/KbaCreateCallbackTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 ForgeRock. All rights reserved. + * Copyright (c) 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -7,18 +7,17 @@ package org.forgerock.android.auth.callback; +import static org.assertj.core.api.Assertions.assertThat; + import org.forgerock.android.auth.FRSession; import org.forgerock.android.auth.Node; import org.forgerock.android.auth.NodeListenerFuture; -import org.forgerock.android.auth.PlatformUsernamePasswordNodeListener; import org.forgerock.android.auth.TreeTest; import org.forgerock.android.auth.UsernamePasswordNodeListener; import java.util.List; import java.util.concurrent.ExecutionException; -import static org.assertj.core.api.Assertions.assertThat; - public class KbaCreateCallbackTest extends TreeTest { private int hit = 0; diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/KeyAttestationTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/KeyAttestationTest.java similarity index 97% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/KeyAttestationTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/KeyAttestationTest.java index 5824f429..64f99575 100644 --- a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/KeyAttestationTest.java +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/KeyAttestationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2022 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -13,6 +13,7 @@ import androidx.test.core.app.ActivityScenario; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SdkSuppress; import com.nimbusds.jose.util.Base64; import com.nimbusds.jwt.JWT; @@ -33,11 +34,11 @@ import org.junit.runner.RunWith; import java.text.ParseException; -import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; @RunWith(AndroidJUnit4.class) +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) // Key Attestation is only supported on Android 7.0 (API level 24) and above... public class KeyAttestationTest extends BaseDeviceBindingTest { protected final static String TREE = "key-attestation"; @@ -70,7 +71,7 @@ public void onSuccess(Void result) { assertThat(x5c).isNull(); // When Android Key Attestation property is set to NONE in AM /// Assert some other properties - assertThat(jwt.getJWTClaimsSet().getClaim("iss")).isEqualTo("org.forgerock.android.auth.test"); + assertThat(jwt.getJWTClaimsSet().getClaim("iss")).isEqualTo("org.forgerock.android.integration.test"); assertThat(jwt.getJWTClaimsSet().getClaim("platform")).isEqualTo("android"); assertThat(jwt.getJWTClaimsSet().getClaim("android-version")).isEqualTo(Long.valueOf(Build.VERSION.SDK_INT)); @@ -129,7 +130,7 @@ public void onSuccess(Void result) { assertThat(x5c).isNotNull(); // When Android Key Attestation is set to DEFAULT in AM /// Assert some other properties - assertThat(jwt.getJWTClaimsSet().getClaim("iss")).isEqualTo("org.forgerock.android.auth.test"); + assertThat(jwt.getJWTClaimsSet().getClaim("iss")).isEqualTo("org.forgerock.android.integration.test"); assertThat(jwt.getJWTClaimsSet().getClaim("platform")).isEqualTo("android"); assertThat(jwt.getJWTClaimsSet().getClaim("android-version")).isEqualTo(Long.valueOf(Build.VERSION.SDK_INT)); } catch (ParseException e) { @@ -157,7 +158,7 @@ public void onException(Exception e) { Assert.assertNotNull(FRSession.getCurrentSession()); Assert.assertNotNull(FRSession.getCurrentSession().getSessionToken()); } - + @Test public void testKeyAttestationTransientStateVariable() throws ExecutionException, InterruptedException { // Ensure that when Key Attestation toggle button is enabled in the Device Binding node, @@ -190,7 +191,7 @@ public void onSuccess(Void result) { assertThat(x5c).isNotNull(); // When Android Key Attestation is set to CUSTOM in AM /// Assert some other properties - assertThat(jwt.getJWTClaimsSet().getClaim("iss")).isEqualTo("org.forgerock.android.auth.test"); + assertThat(jwt.getJWTClaimsSet().getClaim("iss")).isEqualTo("org.forgerock.android.integration.test"); assertThat(jwt.getJWTClaimsSet().getClaim("platform")).isEqualTo("android"); assertThat(jwt.getJWTClaimsSet().getClaim("android-version")).isEqualTo(Long.valueOf(Build.VERSION.SDK_INT)); } catch (ParseException e) { @@ -326,7 +327,7 @@ public void onSuccess(Void result) { assertThat(x5c).isNull(); // When Android Key Attestation is set to NONE in AM /// Assert some other properties - assertThat(jwt.getJWTClaimsSet().getClaim("iss")).isEqualTo("org.forgerock.android.auth.test"); + assertThat(jwt.getJWTClaimsSet().getClaim("iss")).isEqualTo("org.forgerock.android.integration.test"); assertThat(jwt.getJWTClaimsSet().getClaim("platform")).isEqualTo("android"); assertThat(jwt.getJWTClaimsSet().getClaim("android-version")).isEqualTo(Long.valueOf(Build.VERSION.SDK_INT)); @@ -418,7 +419,6 @@ public void onException(Exception e) { assertThat(unsupportedOutcome[0]).isEqualTo(1); assertThat(executionExceptionOccurred).isTrue(); } - } diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/NameCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/NameCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/NameCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/NameCallbackTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/NumberAttributeInputCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/NumberAttributeInputCallbackTest.java similarity index 93% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/NumberAttributeInputCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/NumberAttributeInputCallbackTest.java index 9d068474..3c3ea284 100644 --- a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/NumberAttributeInputCallbackTest.java +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/NumberAttributeInputCallbackTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 ForgeRock. All rights reserved. + * Copyright (c) 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -7,19 +7,16 @@ package org.forgerock.android.auth.callback; -import org.assertj.core.api.Assertions; +import static org.assertj.core.api.Assertions.assertThat; + import org.forgerock.android.auth.FRSession; import org.forgerock.android.auth.Node; import org.forgerock.android.auth.NodeListenerFuture; import org.forgerock.android.auth.TreeTest; import org.forgerock.android.auth.UsernamePasswordNodeListener; -import org.json.JSONException; -import java.util.List; import java.util.concurrent.ExecutionException; -import static org.assertj.core.api.Assertions.assertThat; - public class NumberAttributeInputCallbackTest extends TreeTest { private int hit = 0; diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/PageCallback65Test.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PageCallback65Test.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/PageCallback65Test.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PageCallback65Test.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/PageCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PageCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/PageCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PageCallbackTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/PasswordCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PasswordCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/PasswordCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PasswordCallbackTest.java diff --git a/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PingOneProtectEvaluateCallbackTest.kt b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PingOneProtectEvaluateCallbackTest.kt new file mode 100644 index 00000000..a0ba09f2 --- /dev/null +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PingOneProtectEvaluateCallbackTest.kt @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth.callback + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.forgerock.android.auth.FRSession +import org.forgerock.android.auth.Node +import org.forgerock.android.auth.NodeListener +import org.forgerock.android.auth.PingOneProtectEvaluationCallback +import org.forgerock.android.auth.PingOneProtectInitializeCallback +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PingOneProtectEvaluateCallbackTest : BasePingOneProtectTest() { + private val authenticationTree = "TEST_PING_ONE_PROTECT_EVALUATE" + + @Test + fun test01EvaluateNoInit() { + var evaluateFailure = false + val nodeListenerFuture: PingOneProtectNodeListener = object : PingOneProtectNodeListener( + context, "evaluate-no-init" + ) { + val nodeListener: NodeListener = this + override fun onCallbackReceived(node: Node) { + node.getCallback(PingOneProtectEvaluationCallback::class.java)?.let { + val scope = CoroutineScope(Dispatchers.Default) + scope.launch { + try { + it.getData(context) + } + catch (e: Exception) { + evaluateFailure = true + assertThat("PingOneSignals SDK is not initialized", e.message === "PingOneSignals SDK is not initialized") + } + node.next(context, nodeListener) + } + return + } + + node.getCallback(TextOutputCallback::class.java)?.let { + val callback = node.getCallback( + TextOutputCallback::class.java + ) + + assertThat("The Protect Evaluation node should trigger the Client Error outcome", callback.message == "Client Error") + node.next(context, nodeListener) + return + } + super.onCallbackReceived(node) + } + } + + FRSession.authenticate(context, authenticationTree, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + Assert.assertTrue(evaluateFailure) + // Ensure that the journey finishes with success + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } + + @Test + fun test02EvaluateSuccess() { + var evaluateSuccess = false + val nodeListenerFuture: PingOneProtectNodeListener = object : PingOneProtectNodeListener( + context, "evaluate-default" + ) { + val nodeListener: NodeListener = this + override fun onCallbackReceived(node: Node) { + node.getCallback(PingOneProtectInitializeCallback::class.java)?.let { + val scope = CoroutineScope(Dispatchers.Default) + scope.launch { + try { + it.start(context) + } + catch (e: Exception) { + Assert.fail("Unexpected failure during initialize!") + } + node.next(context, nodeListener) + } + return + } + + node.getCallback(PingOneProtectEvaluationCallback::class.java)?.let { + assertThat("pauseBehavioralData is true", it.pauseBehavioralData == true) + + var signalsData = "" + val scope = CoroutineScope(Dispatchers.Default) + scope.launch { + try { + it.getData(context) + // Make sure that the SDK has collected and sends the signals data + signalsData = it.getInputValue(0).toString() + } + catch (e: Exception) { + Assert.fail("Unexpected failure during evaluate!") + } + + assertThat("Signals data is not empty after collection", signalsData != "") + node.next(context, nodeListener) + } + return + } + + node.getCallback(TextOutputCallback::class.java)?.let { + assertThat("The Protect Evaluation node should trigger High, Medium or Low outcome", it.message == "Success") + evaluateSuccess = true + node.next(context, nodeListener) + return + } + super.onCallbackReceived(node) + } + } + FRSession.authenticate(context, authenticationTree, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + Assert.assertTrue(evaluateSuccess) + + // Ensure that the journey finishes with success + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } + + @Test + fun test03EvaluatePauseBehavioralDataOff() { + var evaluateSuccess = false + val nodeListenerFuture: PingOneProtectNodeListener = object : PingOneProtectNodeListener( + context, "evaluate-pause-off" + ) { + val nodeListener: NodeListener = this + override fun onCallbackReceived(node: Node) { + node.getCallback(PingOneProtectInitializeCallback::class.java)?.let { + val scope = CoroutineScope(Dispatchers.Default) + scope.launch { + try { + it.start(context) + } + catch (e: Exception) { + Assert.fail("Unexpected failure during initialize!") + } + node.next(context, nodeListener) + } + return + } + + node.getCallback(PingOneProtectEvaluationCallback::class.java)?.let { + assertThat("pauseBehavioralData is false", it.pauseBehavioralData == false) + + val scope = CoroutineScope(Dispatchers.Default) + scope.launch { + try { + it.getData(context) + } + catch (e: Exception) { + Assert.fail("Unexpected failure during evaluate!") + } + + // Make sure that the SDK has collected and sends the signals data + assertThat("Signals data is not empty", it.getInputValue(0) != "") + node.next(context, nodeListener) + } + return + } + + node.getCallback(TextOutputCallback::class.java)?.let { + assertThat("The Protect Evaluation node should trigger High, Medium or Low outcome", it.message == "Success") + evaluateSuccess = true + node.next(context, nodeListener) + return + } + super.onCallbackReceived(node) + } + } + + FRSession.authenticate(context, authenticationTree, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + Assert.assertTrue(evaluateSuccess) + + // Ensure that the journey finishes with success + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } +} \ No newline at end of file diff --git a/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PingOneProtectInitializeCallbackTest.kt b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PingOneProtectInitializeCallbackTest.kt new file mode 100644 index 00000000..29abea83 --- /dev/null +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PingOneProtectInitializeCallbackTest.kt @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth.callback + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.forgerock.android.auth.FRSession +import org.forgerock.android.auth.Node +import org.forgerock.android.auth.NodeListener +import org.forgerock.android.auth.PingOneProtectInitializeCallback +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PingOneProtectInitializeCallbackTest : BasePingOneProtectTest() { + private val authenticationTree = "TEST_PING_ONE_PROTECT_INITIALIZE" + + @Test + fun testProtectInitializeDefaults() { + val nodeListenerFuture: PingOneProtectNodeListener = object : PingOneProtectNodeListener( + context, "init-default" + ) { + val nodeListener: NodeListener = this + override fun onCallbackReceived(node: Node) { + node.getCallback(PingOneProtectInitializeCallback::class.java)?.let { + assertThat("envId should not be empty string", it.envId != "") + assertThat("consoleLogEnabled should be false", it.consoleLogEnabled == false) + assertThat("deviceAttributesToIgnore should be an empty list", it.deviceAttributesToIgnore?.isEmpty() ?: false) + assertThat("customHost should be empty string", it.customHost == "") + assertThat("lazyMetadata should be false", it.lazyMetadata == false) + assertThat("behavioralDataCollection should be true", it.behavioralDataCollection == true) + + node.next(context, nodeListener) + return + } + super.onCallbackReceived(node) + } + } + FRSession.authenticate(context, authenticationTree, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + + // Ensure that the journey finishes with success + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } + + @Test + fun testProtectInitializeCustom() { + val nodeListenerFuture: PingOneProtectNodeListener = object : PingOneProtectNodeListener( + context, "init-custom" + ) { + val nodeListener: NodeListener = this + override fun onCallbackReceived(node: Node) { + node.getCallback(PingOneProtectInitializeCallback::class.java)?.let { + assertThat("envId should not be empty string", it.envId != "") + assertThat("consoleLogEnabled should be true", it.consoleLogEnabled == true) + assertThat("deviceAttributesToIgnore contains 'Model'", + it.deviceAttributesToIgnore!!.contains("Model") ); + assertThat("deviceAttributesToIgnore contains 'Manufacturer'", + it.deviceAttributesToIgnore!!.contains("Manufacturer") ); + assertThat("deviceAttributesToIgnore contains 'Screen size'", + it.deviceAttributesToIgnore!!.contains("Screen size") ); + assertThat("customHost should be empty string", it.customHost == "custom.host.com") + assertThat("lazyMetadata should be true", it.lazyMetadata == true) + assertThat("behavioralDataCollection should be false", it.behavioralDataCollection == false) + + node.next(context, nodeListener) + return + } + super.onCallbackReceived(node) + } + } + FRSession.authenticate(context, authenticationTree, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + + // Ensure that the journey finishes with success + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } + + @Test + fun testProtectInitializeClientError() { + var failureTriggered = false + + val nodeListenerFuture: PingOneProtectNodeListener = object : PingOneProtectNodeListener( + context, "init-error" + ) { + val nodeListener: NodeListener = this + override fun onCallbackReceived(node: Node) { + node.getCallback(PingOneProtectInitializeCallback::class.java)?.let { + it.setClientError("Failed to initialize") + + node.next(context, nodeListener) + return + } + node.getCallback(TextOutputCallback::class.java)?.let { + assertThat("The Protect Initialize node should trigger the 'false' outcome", it.message == "Failure") + failureTriggered = true + node.next(context, nodeListener) + return + } + super.onCallbackReceived(node) + } + } + FRSession.authenticate(context, authenticationTree, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + Assert.assertTrue(failureTriggered) + + // Ensure that the journey finishes with success + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } + + @Test + fun testStartProtect() { + val nodeListenerFuture: PingOneProtectNodeListener = object : PingOneProtectNodeListener( + context, "init-default" + ) { + val nodeListener: NodeListener = this + override fun onCallbackReceived(node: Node) { + node.getCallback(PingOneProtectInitializeCallback::class.java)?.let { + val scope = CoroutineScope(Dispatchers.Default) + scope.launch { + try { + it.start(context) + } + catch (e: Exception) { + Assert.fail("Unexpected failure during initialize!") + } + node.next(context, nodeListener) + } + return + } + + node.getCallback(TextOutputCallback::class.java)?.let { + assertThat("The Protect Initialize node should trigger the 'false' outcome", it.message == "Failure") + node.next(context, nodeListener) + return + } + + super.onCallbackReceived(node) + } + } + FRSession.authenticate(context, authenticationTree, nodeListenerFuture) + Assert.assertNotNull(nodeListenerFuture.get()) + + // Ensure that the journey finishes with success + Assert.assertNotNull(FRSession.getCurrentSession()) + Assert.assertNotNull(FRSession.getCurrentSession().sessionToken) + } +} \ No newline at end of file diff --git a/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PingOneProtectNodeListener.kt b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PingOneProtectNodeListener.kt new file mode 100644 index 00000000..d2d3a2b5 --- /dev/null +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PingOneProtectNodeListener.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth.callback + +import android.content.Context +import org.forgerock.android.auth.FRSession +import org.forgerock.android.auth.Node +import org.forgerock.android.auth.NodeListenerFuture + +open class PingOneProtectNodeListener( + private val context: Context, + private val nodeConfiguration: String +) : NodeListenerFuture() { + override fun onCallbackReceived(node: Node) { + node.getCallback(ChoiceCallback::class.java)?.let { + val choices = it.choices + val choiceIndex = choices.indexOf(nodeConfiguration) + it.setSelectedIndex(choiceIndex) + node.next(context, this) + } + node.getCallback(NameCallback::class.java)?.let { + it.setName(BasePingOneProtectTest.USERNAME) + node.next(context, this) + } + } +} \ No newline at end of file diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/PollingWaitCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PollingWaitCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/PollingWaitCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/PollingWaitCallbackTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/ReCaptchaCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ReCaptchaCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/ReCaptchaCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ReCaptchaCallbackTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/StringAttributeInputCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/StringAttributeInputCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/StringAttributeInputCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/StringAttributeInputCallbackTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/TermsAndConditionCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/TermsAndConditionCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/TermsAndConditionCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/TermsAndConditionCallbackTest.java diff --git a/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/TextInputCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/TextInputCallbackTest.java new file mode 100644 index 00000000..f9a6ea9e --- /dev/null +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/TextInputCallbackTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package org.forgerock.android.auth.callback; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.forgerock.android.auth.AndroidBaseTest; +import org.forgerock.android.auth.FRSession; +import org.forgerock.android.auth.Node; +import org.forgerock.android.auth.NodeListener; +import org.forgerock.android.auth.NodeListenerFuture; +import org.forgerock.android.auth.UsernamePasswordNodeListener; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.ExecutionException; + +public class TextInputCallbackTest extends AndroidBaseTest { + protected final static String TREE = "TextInputCallbackTest"; + + @Test + public void testTextInputCallback() throws ExecutionException, InterruptedException { + final int[] textInputCallbackReceived = {0}; + final int[] success = {0}; + NodeListenerFuture nodeListenerFuture = new UsernamePasswordNodeListener(context) { + final NodeListener nodeListener = this; + + @Override + public void onCallbackReceived(Node node) { + if (node.getCallback(NameCallback.class) != null) { + NameCallback nameCallback = node.getCallback(NameCallback.class); + nameCallback.setValue(USERNAME); + node.next(context, this ); + return; + } + if (node.getCallback(TextInputCallback.class) != null) { + TextInputCallback callback = node.getCallback(TextInputCallback.class); + assertThat(callback.getPrompt()).isEqualTo("What is your username?"); + assertThat(callback.getDefaultText()).isEqualTo("ForgerRocker"); + textInputCallbackReceived[0]++; + callback.setValue(USERNAME); + node.next(context, nodeListener); + return; + } + // This step here is to ensure that the SDK correctly sets the value in the TextInputCallback... + // The values entered in the NameCallback and TextInputCallback above should match for "success" + if (node.getCallback(TextOutputCallback.class) != null) { + TextOutputCallback callback = node.getCallback(TextOutputCallback.class); + assertThat(callback.getMessage()).isEqualTo("Success"); + success[0]++; + node.next(context, nodeListener); + return; + } + super.onCallbackReceived(node); + } + }; + + FRSession.authenticate(context, TREE, nodeListenerFuture); + Assert.assertNotNull(nodeListenerFuture.get()); + assertThat(textInputCallbackReceived[0]).isEqualTo(1); + assertThat(success[0]).isEqualTo(1); + + // Ensure that the journey finishes with success + Assert.assertNotNull(FRSession.getCurrentSession()); + Assert.assertNotNull(FRSession.getCurrentSession().getSessionToken()); + } +} diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/ValidatedCreateUsernameCallbackTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ValidatedCreateUsernameCallbackTest.java similarity index 100% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/callback/ValidatedCreateUsernameCallbackTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/callback/ValidatedCreateUsernameCallbackTest.java diff --git a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/collector/FRDeviceIdentifierTest.java b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/collector/FRDeviceIdentifierTest.java similarity index 93% rename from forgerock-auth/src/androidTest/java/org/forgerock/android/auth/collector/FRDeviceIdentifierTest.java rename to forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/collector/FRDeviceIdentifierTest.java index e9f9b65b..2bef0693 100644 --- a/forgerock-auth/src/androidTest/java/org/forgerock/android/auth/collector/FRDeviceIdentifierTest.java +++ b/forgerock-integration-tests/src/androidTest/java/org/forgerock/android/auth/collector/FRDeviceIdentifierTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 - 2022 ForgeRock. All rights reserved. + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -7,8 +7,14 @@ package org.forgerock.android.auth.collector; +import static org.forgerock.android.auth.Encryptor.ANDROID_KEYSTORE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + import android.provider.Settings; + import androidx.test.ext.junit.runners.AndroidJUnit4; + import org.forgerock.android.auth.AndroidBaseTest; import org.forgerock.android.auth.Config; import org.forgerock.android.auth.DeviceIdentifier; @@ -20,9 +26,6 @@ import java.security.GeneralSecurityException; import java.security.KeyStore; -import static org.forgerock.android.auth.Encryptor.ANDROID_KEYSTORE; -import static org.junit.Assert.*; - @RunWith(AndroidJUnit4.class) public class FRDeviceIdentifierTest extends AndroidBaseTest { diff --git a/forgerock-auth/src/androidTest/res/values/strings.xml b/forgerock-integration-tests/src/androidTest/res/values/strings.xml similarity index 86% rename from forgerock-auth/src/androidTest/res/values/strings.xml rename to forgerock-integration-tests/src/androidTest/res/values/strings.xml index 0cbd281f..f670ebbd 100644 --- a/forgerock-auth/src/androidTest/res/values/strings.xml +++ b/forgerock-integration-tests/src/androidTest/res/values/strings.xml @@ -14,15 +14,15 @@ AndroidTest org.forgerock.demo:/oauth2redirect openid profile email address phone - https://openam-forgerrock-sdksteanant.forgeblocks.com/am + https://openam-sdks.forgeblocks.com/am 30 - https://openam-forgerrock-sdksteanant.forgeblocks.com/am + https://openam-sdks.forgeblocks.com/am alpha 30 - iPlanetDirectoryPro + 5421aeddf91aa20 NamePasswordCallbackTest diff --git a/forgerock-auth/src/androidTest/resources/success.json b/forgerock-integration-tests/src/androidTest/resources/success.json similarity index 100% rename from forgerock-auth/src/androidTest/resources/success.json rename to forgerock-integration-tests/src/androidTest/resources/success.json diff --git a/forgerock-auth/src/androidTest/resources/tree/Simple.png b/forgerock-integration-tests/src/androidTest/resources/tree/Simple.png similarity index 100% rename from forgerock-auth/src/androidTest/resources/tree/Simple.png rename to forgerock-integration-tests/src/androidTest/resources/tree/Simple.png diff --git a/forgerock-auth/src/androidTest/resources/webAuthn_authentication_71.json b/forgerock-integration-tests/src/androidTest/resources/webAuthn_authentication_71.json similarity index 100% rename from forgerock-auth/src/androidTest/resources/webAuthn_authentication_71.json rename to forgerock-integration-tests/src/androidTest/resources/webAuthn_authentication_71.json diff --git a/forgerock-auth/src/androidTest/resources/webAuthn_registration_71.json b/forgerock-integration-tests/src/androidTest/resources/webAuthn_registration_71.json similarity index 100% rename from forgerock-auth/src/androidTest/resources/webAuthn_registration_71.json rename to forgerock-integration-tests/src/androidTest/resources/webAuthn_registration_71.json diff --git a/gradle.properties b/gradle.properties index a2e4c06b..aa7fe60c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,10 +21,8 @@ org.gradle.jvmargs=-Xmx1536m # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official android.useAndroidX=true - GROUP=org.forgerock -VERSION=4.3.1 -VERSION_CODE=20 -android.defaults.buildfeatures.buildconfig=true +VERSION=4.4.0 +VERSION_CODE=21 android.nonTransitiveRClass=false android.nonFinalResIds=false \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..0211c906 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,96 @@ +[versions] +agp = "8.2.2" +annotation = "1.6.0" +appauth = "0.11.1" +appcompat = "1.6.1" +assertj-core = "2.9.1" +bcpkix-jdk15on = "1.58.0.0" +biometric-ktx = "1.2.0-alpha05" +commons-io = "2.6" +constraintlayout = "2.1.4" +easy-random-core = "4.0.0" +facebook-login = "16.0.0" +firebase-messaging = "23.1.2" +fragment-ktx = "1.6.1" +integrity = "1.3.0" +kotlin = "1.9.22" +junit = "4.13.2" +androidx-test-core = "1.5.0" +androidx-test-ext-junit = "1.1.5" +androidx-test-runner = "1.5.2" +kotlinx-coroutines-play-services = "1.7.2" +lombok-version = "1.18.28" +mockito-core = "4.8.1" +mockito-kotlin = "4.0.0" +mockwebserver = "2.7.5" +mockwebserver-version = "4.8.0" +nimbus-jose-jwt = "9.37.3" +okhttp = "4.11.0" +org-robolectric-robolectric = "4.9.2" +espresso-core = "3.5.1" +io-mockk = "1.13.9" +org-jetbrains-kotlinx = "1.7.2" +com-pingidentity-signals = "5.1.2" +core-ktx = "1.12.0" +material = "1.11.0" +play-services-auth = "20.6.0" +play-services-fido = "20.0.1" +play-services-location = "21.0.1" +play-services-safetynet = "18.0.1" +powermock-module-junit4 = "2.0.9" +powermock-api-mockito2 = "2.0.9" +rules = "1.5.0" +security-crypto = "1.1.0-alpha06" +firebase-crashlytics-buildtools = "2.9.9" + +[libraries] +androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometric-ktx" } +androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } +androidx-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espresso-core" } +androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment-ktx" } +androidx-fragment-testing = { module = "androidx.fragment:fragment-testing", version.ref = "fragment-ktx" } +androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "security-crypto" } +appauth = { module = "net.openid:appauth", version.ref = "appauth" } +assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj-core" } +bcpkix-jdk15on = { module = "com.madgag.spongycastle:bcpkix-jdk15on", version.ref = "bcpkix-jdk15on" } +commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" } +easy-random-core = { module = "org.jeasy:easy-random-core", version.ref = "easy-random-core" } +facebook-login = { module = "com.facebook.android:facebook-login", version.ref = "facebook-login" } +firebase-messaging = { module = "com.google.firebase:firebase-messaging", version.ref = "firebase-messaging" } +integrity = { module = "com.google.android.play:integrity", version.ref = "integrity" } +jetbrains-kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlinx-coroutines-play-services" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } +androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidx-test-core" } +androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidx-test-runner" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "org-jetbrains-kotlinx" } +logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } +mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito-kotlin" } +mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito-core" } +mockwebserver = { module = "com.squareup.okhttp:mockwebserver", version.ref = "mockwebserver" } +nimbus-jose-jwt = { module = "com.nimbusds:nimbus-jose-jwt", version.ref = "nimbus-jose-jwt" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +okhttp3-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "mockwebserver-version" } +org-robolectric-robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "org-robolectric-robolectric" } +espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } +io-mockk = { group = "io.mockk", name = "mockk", version.ref = "io-mockk" } +org-jetbrains-kotlinx = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "org-jetbrains-kotlinx" } +com-pingidentity-signals = { group = "com.pingidentity.signals", name = "android-sdk", version.ref = "com-pingidentity-signals" } +appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +play-services-auth = { module = "com.google.android.gms:play-services-auth", version.ref = "play-services-auth" } +play-services-fido = { module = "com.google.android.gms:play-services-fido", version.ref = "play-services-fido" } +play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "play-services-location" } +play-services-safetynet = { module = "com.google.android.gms:play-services-safetynet", version.ref = "play-services-safetynet" } +powermock-api-mockito2 = { module = "org.powermock:powermock-api-mockito2", version.ref = "powermock-api-mockito2" } +powermock-module-junit4 = { module = "org.powermock:powermock-module-junit4", version.ref = "powermock-module-junit4" } +projectlombok-lombok = { module = "org.projectlombok:lombok", version.ref = "lombok-version" } +rules = { module = "androidx.test:rules", version.ref = "rules" } +firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebase-crashlytics-buildtools" } + +[plugins] +androidLibrary = { id = "com.android.library", version.ref = "agp" } +kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b354f1cf..c702fbca 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Aug 16 10:44:41 PDT 2021 +#Wed Feb 21 12:34:24 PST 2024 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/ping-protect/.gitignore b/ping-protect/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/ping-protect/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/ping-protect/build.gradle.kts b/ping-protect/build.gradle.kts new file mode 100644 index 00000000..862096e2 --- /dev/null +++ b/ping-protect/build.gradle.kts @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +plugins { + alias(libs.plugins.androidLibrary) + alias(libs.plugins.kotlinAndroid) + id("com.adarshr.test-logger") + id("maven-publish") + id("signing") + id("org.jetbrains.dokka") +} + +apply() + +android { + namespace = "org.forgerock.android.protect" +} + +tasks.dokkaHtml.configure { + outputDirectory.set(file("$buildDir/html")) +} + +tasks.dokkaJavadoc.configure { + outputDirectory.set(file("$buildDir/javadoc")) +} + +/** + * JCenter Dependency Manager + */ +tasks { + val sourcesJar by creating(Jar::class) { + archiveClassifier.set("sources") + from(android.sourceSets.getByName("main").java.srcDirs) + } + + val javadocJar by creating(Jar::class) { + dependsOn.add(dokkaHtml) + archiveClassifier.set("javadoc") + from(File("$buildDir/generated-javadoc")) + } + + artifacts { + archives(sourcesJar) + archives(javadocJar) + } +} + +apply("../config/logger.gradle") +apply("../config/publish.gradle") + +dependencies { + implementation(project(":forgerock-auth")) + implementation(libs.com.pingidentity.signals) + implementation(libs.org.jetbrains.kotlinx) + + testImplementation(libs.org.robolectric.robolectric) + testImplementation(libs.junit) + testImplementation(libs.androidx.test.core) + testImplementation(libs.androidx.test.runner) + testImplementation(libs.androidx.test.ext.junit) + testImplementation(libs.espresso.core) + + // Mockk + testImplementation(libs.io.mockk) + + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.espresso.core) +} \ No newline at end of file diff --git a/ping-protect/consumer-rules.pro b/ping-protect/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/ping-protect/gradle.properties b/ping-protect/gradle.properties new file mode 100644 index 00000000..aad29c13 --- /dev/null +++ b/ping-protect/gradle.properties @@ -0,0 +1,27 @@ +# +# Copyright (c) 2024 ForgeRock. All rights reserved. +# +# This software may be modified and distributed under the terms +# of the MIT license. See the LICENSE file for details. +# + +POM_ARTIFACT_ID=ping-protect + +POM_NAME=ping-protect +POM_PACKAGING=aar + +POM_DESCRIPTION=Forgerock Android SDK +POM_INCEPTION_YEAR=2024 + +POM_URL=https://github.com/ForgeRock/forgerock-android-sdk +POM_SCM_URL=https://github.com/ForgeRock/forgerock-android-sdk.git +POM_SCM_CONNECTION=scm:git@github.com:ForgeRock/forgerock-android-sdk.git +POM_SCM_DEV_CONNECTION=scm:git@github.com:ForgeRock/forgerock-android-sdk.git + +POM_LICENCE_NAME=MIT +POM_LICENCE_URL=https://opensource.org/licenses/MIT +POM_LICENCE_DIST=repo + +POM_DEVELOPER_ID=witrisna +POM_DEVELOPER_NAME=Andy Witrisna +POM_DEVELOPER_URL=https://github.com/witrisna \ No newline at end of file diff --git a/ping-protect/proguard-rules.pro b/ping-protect/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/ping-protect/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/ping-protect/src/main/AndroidManifest.xml b/ping-protect/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1215bdbc --- /dev/null +++ b/ping-protect/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/ping-protect/src/main/java/org/forgerock/android/auth/PIProtect.kt b/ping-protect/src/main/java/org/forgerock/android/auth/PIProtect.kt new file mode 100644 index 00000000..5b3a954e --- /dev/null +++ b/ping-protect/src/main/java/org/forgerock/android/auth/PIProtect.kt @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth + +import android.content.Context +import com.pingidentity.signalssdk.sdk.GetDataCallback +import com.pingidentity.signalssdk.sdk.InitCallback +import com.pingidentity.signalssdk.sdk.POInitParams +import com.pingidentity.signalssdk.sdk.PingOneSignals +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +/** + * PIProtect is for initializing and interacting with Ping Protect SDK + */ +class PIProtect { + companion object { + + const val TAG: String = "PIProtect" + + // Save the init state of the protect. + internal var protectParamState: POInitParams? = null + + /** + * Initialize Ping Protect SDK + * + * @param context The Application Context + * @param parameter The `PIInitParams` containing parameters for the init + */ + @JvmStatic + suspend fun start( + context: Context, + parameter: PIInitParams? = null, + ) { + return suspendCancellableCoroutine { init -> + protectParamState?.let { + init.resume(Unit) + } ?: run { + val outputParam = getInitParam(parameter) + PingOneSignals.setInitCallback( + object : InitCallback { + override fun onInitialized() { + Logger.info(TAG, "PingOneSignals Initialized") + protectParamState = outputParam + init.resume(Unit) + } + override fun onError( + p0: String, + p1: String, + p2: String, + ) { + Logger.error(TAG, "PingOneSignals failed $p0 $p1 $p2") + protectParamState = null + init.resumeWithException(PingOneProtectInitException("PingOneSignals failed $p0 $p1 $p2")) + } + }, + ) + PingOneSignals.init(context, outputParam) + } + } + } + + private fun getInitParam(parameter: PIInitParams? = null): POInitParams { + val init: POInitParams = parameter?.let { + val params = POInitParams() + params.apply { + envId = parameter.envId + isBehavioralDataCollection = parameter.isBehavioralDataCollection + isConsoleLogEnabled = parameter.isConsoleLogEnabled + customHost = parameter.customHost + isLazyMetadata = parameter.isLazyMetadata + deviceAttributesToIgnore = parameter.deviceAttributesToIgnore + } + } ?: POInitParams() + return init + } + + /** + * Get the signal data from PingSDK + * + */ + internal suspend fun getData(): String { + return suspendCancellableCoroutine { + PingOneSignals.getData( + object : GetDataCallback { + override fun onSuccess(result: String) { + it.resume(result) + } + + override fun onFailure(result: String) { + it.resumeWithException(PingOneProtectEvaluationException(result)) + } + }, + ) + } + } + + /** + * Pause behavioral data collection + */ + fun pauseBehavioralData() { + PingOneSignals.pauseBehavioralData() + } + + /** + * Resume behavioral data collection + */ + fun resumeBehavioralData() { + PingOneSignals.resumeBehavioralData() + } + } +} + +/** + * Parameters for starting PIProtect SDK + */ +data class PIInitParams( + val envId: String? = null, + val deviceAttributesToIgnore: List? = null, + val customHost: String? = null, + val isConsoleLogEnabled: Boolean = false, + val isLazyMetadata: Boolean = false, + val isBehavioralDataCollection: Boolean = true, +) diff --git a/ping-protect/src/main/java/org/forgerock/android/auth/PingOneProtectEvaluationCallback.kt b/ping-protect/src/main/java/org/forgerock/android/auth/PingOneProtectEvaluationCallback.kt new file mode 100644 index 00000000..5d281aa8 --- /dev/null +++ b/ping-protect/src/main/java/org/forgerock/android/auth/PingOneProtectEvaluationCallback.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth + +import android.content.Context +import androidx.annotation.Keep +import org.forgerock.android.auth.callback.AbstractCallback +import org.json.JSONObject + +private val TAG = PingOneProtectEvaluationCallback::class.java.simpleName + +/** + * Callback to evaluate Ping One Protect + */ +open class PingOneProtectEvaluationCallback : AbstractCallback { + + @Keep + constructor(jsonObject: JSONObject, index: Int) : super(jsonObject, index) + + @Keep + constructor() : super() + /** + * The pauseBehavioralData received from server + */ + var pauseBehavioralData: Boolean? = null + private set + + final override fun setAttribute(name: String, value: Any) = when (name) { + "pauseBehavioralData" -> pauseBehavioralData = value as? Boolean + else -> {} + } + + override fun getType(): String { + return "PingOneProtectEvaluationCallback" + } + + /** + * Input the Signal to the server + * @param value The JWS value. + */ + fun setSignals(value: String) { + super.setValue(value, 0) + } + + /** + * Input the Client Error to the server + * @param value Protect ErrorType . + */ + fun setClientError(value: String) { + super.setValue(value, 1) + } + + /** + * Get the signal by Calling the [getData] function. + * + * @param context The Application Context + */ + open suspend fun getData(context: Context) { + try { + val result = PIProtect.getData() + if (pauseBehavioralData == true) { + PIProtect.pauseBehavioralData() + } + setSignals(result) + } + catch (e: Exception) { + Logger.error(TAG, t = e, message = e.message) + setClientError(e.message ?: "clientError") + throw e + } + } +} + +/** + * Exception to capture PingOneProtect Signal. + * + * @param message The Message + */ +class PingOneProtectEvaluationException(message: String?) : Exception(message) \ No newline at end of file diff --git a/ping-protect/src/main/java/org/forgerock/android/auth/PingOneProtectInitializeCallback.kt b/ping-protect/src/main/java/org/forgerock/android/auth/PingOneProtectInitializeCallback.kt new file mode 100644 index 00000000..0487363b --- /dev/null +++ b/ping-protect/src/main/java/org/forgerock/android/auth/PingOneProtectInitializeCallback.kt @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth + +import android.content.Context +import androidx.annotation.Keep +import org.forgerock.android.auth.callback.AbstractCallback +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.util.Collections + +private val TAG = PingOneProtectInitializeCallback::class.java.simpleName + +/** + * Callback to initialize the ping one protect + */ +open class PingOneProtectInitializeCallback : AbstractCallback { + @Keep + constructor(jsonObject: JSONObject, index: Int) : super(jsonObject, index) + + @Keep + constructor() : super() + + var envId: String? = null + private set + var behavioralDataCollection: Boolean? = null + private set + + var consoleLogEnabled: Boolean? = null + private set + + var lazyMetadata: Boolean? = null + private set + + var customHost: String? = null + private set + var deviceAttributesToIgnore: List? = null + private set + + /** + * Get the getDeviceAttributes attribute + * + * @param array The data source + */ + private fun getDeviceAttributes(array: JSONArray?): List? { + val list: MutableList = ArrayList() + return array?.let { + for (i in 0 until array.length()) { + try { + list.add(array.getString(i)) + } catch (e: JSONException) { + return null + } + } + return Collections.unmodifiableList(list) + } + } + final override fun setAttribute( + name: String, + value: Any, + ) = when (name) { + "envId" -> envId = value as? String + "behavioralDataCollection" -> behavioralDataCollection = value as? Boolean + "consoleLogEnabled" -> consoleLogEnabled = value as? Boolean + "deviceAttributesToIgnore" -> deviceAttributesToIgnore = getDeviceAttributes(value as? JSONArray) + "customHost" -> customHost = value as? String + "lazyMetadata" -> lazyMetadata = value as? Boolean + else -> {} + } + + override fun getType(): String { + return "PingOneProtectInitializeCallback" + } + + /** + * Input the Client Error to the server + * @param value Protect ErrorType . + */ + fun setClientError(value: String) { + super.setValue(value, 0) + } + + /** + * Collect the behavior. Calling the [start] function. + * + * @param context The Application Context + */ + open suspend fun start(context: Context) { + try { + val isBehavioralEnabled = behavioralDataCollection ?: true + val init = + PIInitParams( + envId = envId, + isBehavioralDataCollection = isBehavioralEnabled, + isLazyMetadata = lazyMetadata ?: false, + isConsoleLogEnabled = consoleLogEnabled ?: false, + deviceAttributesToIgnore = deviceAttributesToIgnore, + customHost = customHost, + ) + PIProtect.start(context, init) + if(isBehavioralEnabled) { + PIProtect.resumeBehavioralData() + } else { + PIProtect.pauseBehavioralData() + } + } catch (e: Exception) { + Logger.error(TAG, t = e, message = e.message) + setClientError(e.message ?: "clientError") + throw e + } + } +} + +/** + * Exception to Init PingOneProtect. + * + * @param message The Message + */ +class PingOneProtectInitException(message: String?) : Exception(message) diff --git a/ping-protect/src/main/java/org/forgerock/android/auth/PingOneProvider.kt b/ping-protect/src/main/java/org/forgerock/android/auth/PingOneProvider.kt new file mode 100644 index 00000000..414f9630 --- /dev/null +++ b/ping-protect/src/main/java/org/forgerock/android/auth/PingOneProvider.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth + +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri +import androidx.annotation.MainThread +import org.forgerock.android.auth.callback.CallbackFactory + +/** + * Content Provider to register Activity Lifecycle Callbacks . + */ +internal class PingOneProvider : ContentProvider() { + + internal var factory: CallbackFactory = CallbackFactory.getInstance() + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + throw UnsupportedOperationException("Not yet implemented") + } + + override fun getType(uri: Uri): String? { + throw UnsupportedOperationException("Not yet implemented") + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + throw UnsupportedOperationException("Not yet implemented") + } + + @MainThread + override fun onCreate(): Boolean { + factory.register(PingOneProtectInitializeCallback::class.java) + factory.register(PingOneProtectEvaluationCallback::class.java) + return false + } + + override fun query( + uri: Uri, projection: Array?, selection: String?, + selectionArgs: Array?, sortOrder: String? + ): Cursor? { + throw UnsupportedOperationException("Not yet implemented") + } + + override fun update( + uri: Uri, values: ContentValues?, selection: String?, + selectionArgs: Array? + ): Int { + throw UnsupportedOperationException("Not yet implemented") + } + +} diff --git a/ping-protect/src/test/java/org/forgerock/android/auth/PIProtectTest.kt b/ping-protect/src/test/java/org/forgerock/android/auth/PIProtectTest.kt new file mode 100644 index 00000000..0c8f596c --- /dev/null +++ b/ping-protect/src/test/java/org/forgerock/android/auth/PIProtectTest.kt @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth + + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.pingidentity.signalssdk.sdk.GetDataCallback +import com.pingidentity.signalssdk.sdk.InitCallback +import com.pingidentity.signalssdk.sdk.POInitParams +import com.pingidentity.signalssdk.sdk.PingOneSignals +import io.mockk.every +import io.mockk.mockkStatic +import io.mockk.slot +import io.mockk.verify +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PIProtectTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + @After + fun tearDown() { + PIProtect.protectParamState = null + } + + @Test + fun testPauseBehaviourDataCalled() { + mockkStatic(PingOneSignals::class) + every { PingOneSignals.pauseBehavioralData() }.returns(Unit) + PIProtect.pauseBehavioralData() + verify { PingOneSignals.pauseBehavioralData() } + } + + @Test + fun testResumeBehaviourDataCalled() { + mockkStatic(PingOneSignals::class) + every { PingOneSignals.resumeBehavioralData() }.returns(Unit) + PIProtect.resumeBehavioralData() + verify { PingOneSignals.resumeBehavioralData() } + } + + @Test + fun testInitSDKDefaultValue() = runBlocking { + mockkStatic(PingOneSignals::class) + val mockSlot = slot() + val mockParamSlot = slot() + every { PingOneSignals.setInitCallback(capture(mockSlot)) }.answers { + mockSlot.captured.onInitialized() + } + every { PingOneSignals.init(context, capture(mockParamSlot)) }.returns(Unit) + assertNull(PIProtect.protectParamState) + PIProtect.start(context) + assertNotNull(PIProtect.protectParamState) + assertEquals(null, mockParamSlot.captured.envId) + assertEquals(false, mockParamSlot.captured.isConsoleLogEnabled) + assertEquals(true, mockParamSlot.captured.isBehavioralDataCollection) + assertEquals(false, mockParamSlot.captured.isLazyMetadata) + assertEquals(null, mockParamSlot.captured.deviceAttributesToIgnore) + assertEquals(null, mockParamSlot.captured.customHost) + verify(exactly = 1) { PingOneSignals.setInitCallback(any()) } + PIProtect.start(context) + verify(exactly = 1) { PingOneSignals.setInitCallback(any()) } + } + + @Test + fun testInitSDKWithSomeValue() = runBlocking { + mockkStatic(PingOneSignals::class) + val mockSlot = slot() + val mockParamSlot = slot() + every { PingOneSignals.setInitCallback(capture(mockSlot)) }.answers { + mockSlot.captured.onInitialized() + } + every { PingOneSignals.init(context, capture(mockParamSlot)) }.returns(Unit) + assertNull(PIProtect.protectParamState) + val init = PIInitParams(envId = "jey", isConsoleLogEnabled = true, deviceAttributesToIgnore = listOf("value")) + PIProtect.start(context, init) + assertNotNull(PIProtect.protectParamState) + assertEquals("jey", mockParamSlot.captured.envId) + assertEquals(true, mockParamSlot.captured.isConsoleLogEnabled) + assertEquals(true, mockParamSlot.captured.isBehavioralDataCollection) + assertEquals(false, mockParamSlot.captured.isLazyMetadata) + assertEquals(listOf("value"), mockParamSlot.captured.deviceAttributesToIgnore) + assertEquals(null, mockParamSlot.captured.customHost) + verify(exactly = 1) { PingOneSignals.setInitCallback(any()) } + PIProtect.start(context) + verify(exactly = 1) { PingOneSignals.setInitCallback(any()) } + } + + @Test + fun testErrorInit() = runBlocking { + mockkStatic(PingOneSignals::class) + val mockSlot = slot() + val mockParamSlot = slot() + every { PingOneSignals.setInitCallback(capture(mockSlot)) }.answers { + mockSlot.captured.onError("error", "init" , "sdk") + } + every { PingOneSignals.init(context, capture(mockParamSlot)) }.returns(Unit) + assertNull(PIProtect.protectParamState) + try{ + PIProtect.start(context) + fail() + } + catch (e: Exception) { + assertNull(PIProtect.protectParamState) + assertTrue(e is PingOneProtectInitException) + } + } + + @Test + fun testGetData() = runBlocking { + mockkStatic(PingOneSignals::class) + every { PingOneSignals.pauseBehavioralData() }.returns(Unit) + val mockSlot = slot() + every { PingOneSignals.getData(capture(mockSlot)) }.answers { + mockSlot.captured.onSuccess("test") + } + val value = PIProtect.getData() + assertEquals("test", value) + } + + @Test + fun testGetFailure() = runBlocking { + mockkStatic(PingOneSignals::class) + every { PingOneSignals.pauseBehavioralData() }.returns(Unit) + val mockSlot = slot() + every { PingOneSignals.getData(capture(mockSlot)) }.answers { + mockSlot.captured.onFailure("test") + } + try { + PIProtect.getData() + fail() + } catch (e: Exception) { + assertTrue(e is PingOneProtectEvaluationException) + } + verify(exactly = 0) { PingOneSignals.pauseBehavioralData() } + } + +} \ No newline at end of file diff --git a/ping-protect/src/test/java/org/forgerock/android/auth/PingOneProtectEvaluationCallbackTest.kt b/ping-protect/src/test/java/org/forgerock/android/auth/PingOneProtectEvaluationCallbackTest.kt new file mode 100644 index 00000000..7a4d6363 --- /dev/null +++ b/ping-protect/src/test/java/org/forgerock/android/auth/PingOneProtectEvaluationCallbackTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockkObject +import kotlinx.coroutines.runBlocking +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PingOneProtectEvaluationCallbackTest { + + val context: Context = ApplicationProvider.getApplicationContext() + + @Before + fun setUp() { + MockKAnnotations.init(this) + } + @Test + fun basicTest() { + val json = "{\"type\":\"PingOneProtectEvaluationCallback\",\"output\":[{\"name\":\"pauseBehavioralData\",\"value\":true}],\"input\":[{\"name\":\"IDToken1signals\",\"value\":\"HcSd8yBF7G0g0d0lzzz6rci4I0IB+VZdFl6bBiRN2RjMhjSsipf0h0JC4ganxwiBRzSoGwMMZagmq7Teoabm6cZ8X0mStPL\\/bzCCAQPc5wmGmW2M7GKEITiEQbgHN+ZB\\/cd8g6MmCJsYK5OYplbG\\/SDuCtWZDIS4mUdxywlTFDMmXm9tC2Fy5vfgk+9DX4eOSPIHiQq5wPpGsILrY17H87A4Qt4gb6ITC2s9Oo7qf8R0gfiJttuPyWYFL7w1KoiuUi6JPf5v2H0HW04Mc1qlZwD44Dd7RlHGQeGs\\/fk21KZ6kKI4cTd8eHAt3Vrl29yIhn6Ce\\/go1Ve\\/0qj0DWx703SRbuc5IBm8AR\\/q9DpxQkEd8PC8+FWBisuGLTQyjqTS6DCEy7LwgR0LU28Hwdw1jDeZgoZy54kCpo6v9B6x1\\/bkNH8YtlSt9uz\\/A9UinS4g0VdN09H6SXKXNxn4bYhJeWK7c4q9Byvuye1M08qh7JWzMKpkWyZwaeC6zIaMQhiwrodyjS+S25dBk1YcQ==.eDE=\"},{\"name\":\"IDToken1clientError\",\"value\":\"\"}]}" + val raw = JSONObject(json) + val pingOneEvalCallback = PingOneProtectEvaluationCallback(raw, 0) + assertEquals(true, + pingOneEvalCallback.pauseBehavioralData) + } + @Test + fun initBehaviorData() = runBlocking { + mockkObject(PIProtect) + coEvery { + PIProtect.getData() + } returns("result") + val json = "{\"type\":\"PingOneProtectEvaluationCallback\",\"output\":[{\"name\":\"pauseBehavioralData\",\"value\":true}],\"input\":[{\"name\":\"IDToken1signals\",\"value\":\"HcSd8yBF7G0g0d0lzzz6rci4I0IB+VZdFl6bBiRN2RjMhjSsipf0h0JC4ganxwiBRzSoGwMMZagmq7Teoabm6cZ8X0mStPL\\/bzCCAQPc5wmGmW2M7GKEITiEQbgHN+ZB\\/cd8g6MmCJsYK5OYplbG\\/SDuCtWZDIS4mUdxywlTFDMmXm9tC2Fy5vfgk+9DX4eOSPIHiQq5wPpGsILrY17H87A4Qt4gb6ITC2s9Oo7qf8R0gfiJttuPyWYFL7w1KoiuUi6JPf5v2H0HW04Mc1qlZwD44Dd7RlHGQeGs\\/fk21KZ6kKI4cTd8eHAt3Vrl29yIhn6Ce\\/go1Ve\\/0qj0DWx703SRbuc5IBm8AR\\/q9DpxQkEd8PC8+FWBisuGLTQyjqTS6DCEy7LwgR0LU28Hwdw1jDeZgoZy54kCpo6v9B6x1\\/bkNH8YtlSt9uz\\/A9UinS4g0VdN09H6SXKXNxn4bYhJeWK7c4q9Byvuye1M08qh7JWzMKpkWyZwaeC6zIaMQhiwrodyjS+S25dBk1YcQ==.eDE=\"},{\"name\":\"IDToken1clientError\",\"value\":\"\"}]}" + val raw = JSONObject(json) + val pingOneEvalCallback = PingOneProtectEvaluationCallback(raw, 0) + pingOneEvalCallback.getData(context) + assertTrue(pingOneEvalCallback.content.contains("result")) + coVerify(exactly = 1) { PIProtect.getData() } + coVerify(exactly = 1) { PIProtect.pauseBehavioralData() } + } + + @Test + fun `test exception`() = runBlocking { + mockkObject(PIProtect) + coEvery { + PIProtect.getData() + } throws(PingOneProtectEvaluationException("evaluation failed")) + + try { + val json = "{\"type\":\"PingOneProtectEvaluationCallback\",\"output\":[{\"name\":\"pauseBehavioralData\",\"value\":true}],\"input\":[{\"name\":\"IDToken1signals\",\"value\":\"HcSd8yBF7G0g0d0lzzz6rci4I0IB+VZdFl6bBiRN2RjMhjSsipf0h0JC4ganxwiBRzSoGwMMZagmq7Teoabm6cZ8X0mStPL\\/bzCCAQPc5wmGmW2M7GKEITiEQbgHN+ZB\\/cd8g6MmCJsYK5OYplbG\\/SDuCtWZDIS4mUdxywlTFDMmXm9tC2Fy5vfgk+9DX4eOSPIHiQq5wPpGsILrY17H87A4Qt4gb6ITC2s9Oo7qf8R0gfiJttuPyWYFL7w1KoiuUi6JPf5v2H0HW04Mc1qlZwD44Dd7RlHGQeGs\\/fk21KZ6kKI4cTd8eHAt3Vrl29yIhn6Ce\\/go1Ve\\/0qj0DWx703SRbuc5IBm8AR\\/q9DpxQkEd8PC8+FWBisuGLTQyjqTS6DCEy7LwgR0LU28Hwdw1jDeZgoZy54kCpo6v9B6x1\\/bkNH8YtlSt9uz\\/A9UinS4g0VdN09H6SXKXNxn4bYhJeWK7c4q9Byvuye1M08qh7JWzMKpkWyZwaeC6zIaMQhiwrodyjS+S25dBk1YcQ==.eDE=\"},{\"name\":\"IDToken1clientError\",\"value\":\"\"}]}" + val raw = JSONObject(json) + val pingOneEvalCallback = PingOneProtectEvaluationCallback(raw, 0) + pingOneEvalCallback.getData(context) + fail() + } catch (e: Exception) { + coVerify(exactly = 1) { PIProtect.getData() } + assertTrue(e is PingOneProtectEvaluationException) + } + } + +} \ No newline at end of file diff --git a/ping-protect/src/test/java/org/forgerock/android/auth/PingOneProtectInitializeCallbackTest.kt b/ping-protect/src/test/java/org/forgerock/android/auth/PingOneProtectInitializeCallbackTest.kt new file mode 100644 index 00000000..e7ff9ec7 --- /dev/null +++ b/ping-protect/src/test/java/org/forgerock/android/auth/PingOneProtectInitializeCallbackTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockkObject +import io.mockk.slot +import kotlinx.coroutines.runBlocking +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PingOneProtectInitializeCallbackTest { + + val context: Context = ApplicationProvider.getApplicationContext() + + @Before + fun setUp() { + MockKAnnotations.init(this) + } + @Test + fun basicTest() { + val json = "{\"type\":\"PingOneProtectInitializeCallback\",\"output\":[{\"name\":\"envId\",\"value\":\"02fb4743-189a-4bc7-9d6c-a919edfe6447\"},{\"name\":\"consoleLogEnabled\",\"value\":true},{\"name\":\"deviceAttributesToIgnore\",\"value\":[]},{\"name\":\"customHost\",\"value\":\"\"},{\"name\":\"lazyMetadata\",\"value\":false},{\"name\":\"behavioralDataCollection\",\"value\":true},{\"name\":\"deviceKeyRsyncIntervals\",\"value\":14},{\"name\":\"enableTrust\",\"value\":false},{\"name\":\"disableTags\",\"value\":false},{\"name\":\"disableHub\",\"value\":false}],\"input\":[{\"name\":\"IDToken1clientError\",\"value\":\"\"}]}" + val raw = JSONObject(json) + val pingOneInitCallback = PingOneProtectInitializeCallback(raw, 0) + assertEquals("02fb4743-189a-4bc7-9d6c-a919edfe6447", + pingOneInitCallback.envId) + assertEquals(true, + pingOneInitCallback.behavioralDataCollection) + assertEquals(true, + pingOneInitCallback.consoleLogEnabled) + } + + @Test + fun basicTestDifferentParam() { + val json = "{\"type\":\"PingOneProtectInitializeCallback\",\"output\":[{\"name\":\"envId\",\"value\":\"02fb4743-189a-4bc7-9d6c-a919edfe6447\"},{\"name\":\"consoleLogEnabled\",\"value\":false},{\"name\":\"deviceAttributesToIgnore\",\"value\":[]},{\"name\":\"customHost\",\"value\":\"\"},{\"name\":\"lazyMetadata\",\"value\":false},{\"name\":\"behavioralDataCollection\",\"value\":false},{\"name\":\"deviceKeyRsyncIntervals\",\"value\":14},{\"name\":\"enableTrust\",\"value\":false},{\"name\":\"disableTags\",\"value\":false},{\"name\":\"disableHub\",\"value\":false}],\"input\":[{\"name\":\"IDToken1clientError\",\"value\":\"\"}]}" + val raw = JSONObject(json) + val pingOneInitCallback = PingOneProtectInitializeCallback(raw, 0) + assertEquals("02fb4743-189a-4bc7-9d6c-a919edfe6447", + pingOneInitCallback.envId) + assertEquals(false, + pingOneInitCallback.behavioralDataCollection) + assertEquals(false, + pingOneInitCallback.consoleLogEnabled) + } + + @Test + fun initMethodCalled() = runBlocking { + mockkObject(PIProtect) + val mockSlot = slot() + coEvery { + PIProtect.start(context, capture(mockSlot)) + } returns(Unit) + coEvery { + PIProtect.resumeBehavioralData() + } returns(Unit) + coEvery { + PIProtect.pauseBehavioralData() + } returns(Unit) + try { + val json = + "{\"type\":\"PingOneProtectInitializeCallback\",\"output\":[{\"name\":\"envId\",\"value\":\"02fb4743-189a-4bc7-9d6c-a919edfe6447\"},{\"name\":\"consoleLogEnabled\",\"value\":false},{\"name\":\"deviceAttributesToIgnore\",\"value\":[\"value1\", \"value2\"]},{\"name\":\"customHost\",\"value\":\"\"},{\"name\":\"lazyMetadata\",\"value\":false},{\"name\":\"behavioralDataCollection\",\"value\":true},{\"name\":\"deviceKeyRsyncIntervals\",\"value\":14},{\"name\":\"enableTrust\",\"value\":false},{\"name\":\"disableTags\",\"value\":false},{\"name\":\"disableHub\",\"value\":false}],\"input\":[{\"name\":\"IDToken1clientError\",\"value\":\"\"}]}" + val raw = JSONObject(json) + val pingOneInitCallback = PingOneProtectInitializeCallback(raw, 0) + pingOneInitCallback.start(context) + coVerify { PIProtect.start(context, any()) } + assertEquals(mockSlot.captured.envId, "02fb4743-189a-4bc7-9d6c-a919edfe6447") + assertEquals(mockSlot.captured.isBehavioralDataCollection, true) + assertEquals(mockSlot.captured.isConsoleLogEnabled, false) + assertEquals(mockSlot.captured.isLazyMetadata, false) + assertEquals(mockSlot.captured.customHost, "") + assertEquals(mockSlot.captured.deviceAttributesToIgnore, listOf("value1", "value2")) + coVerify(exactly = 1) { PIProtect.resumeBehavioralData() } + coVerify(exactly = 0) { PIProtect.pauseBehavioralData() } + } catch (e: Exception) { + fail() + } + } + + @Test + fun initMethodCalledPause() = runBlocking { + mockkObject(PIProtect) + val mockSlot = slot() + coEvery { + PIProtect.start(context, capture(mockSlot)) + } returns(Unit) + coEvery { + PIProtect.resumeBehavioralData() + } returns(Unit) + coEvery { + PIProtect.pauseBehavioralData() + } returns(Unit) + try { + val json = + "{\"type\":\"PingOneProtectInitializeCallback\",\"output\":[{\"name\":\"envId\",\"value\":\"02fb4743-189a-4bc7-9d6c-a919edfe6447\"},{\"name\":\"consoleLogEnabled\",\"value\":false},{\"name\":\"deviceAttributesToIgnore\",\"value\":[\"value1\", \"value2\"]},{\"name\":\"customHost\",\"value\":\"\"},{\"name\":\"lazyMetadata\",\"value\":false},{\"name\":\"behavioralDataCollection\",\"value\":false},{\"name\":\"deviceKeyRsyncIntervals\",\"value\":14},{\"name\":\"enableTrust\",\"value\":false},{\"name\":\"disableTags\",\"value\":false},{\"name\":\"disableHub\",\"value\":false}],\"input\":[{\"name\":\"IDToken1clientError\",\"value\":\"\"}]}" + val raw = JSONObject(json) + val pingOneInitCallback = PingOneProtectInitializeCallback(raw, 0) + pingOneInitCallback.start(context) + coVerify { PIProtect.start(context, any()) } + assertEquals(mockSlot.captured.envId, "02fb4743-189a-4bc7-9d6c-a919edfe6447") + assertEquals(mockSlot.captured.isBehavioralDataCollection, false) + assertEquals(mockSlot.captured.isConsoleLogEnabled, false) + assertEquals(mockSlot.captured.isLazyMetadata, false) + assertEquals(mockSlot.captured.customHost, "") + assertEquals(mockSlot.captured.deviceAttributesToIgnore, listOf("value1", "value2")) + coVerify(exactly = 0) { PIProtect.resumeBehavioralData() } + coVerify(exactly = 1) { PIProtect.pauseBehavioralData() } + } catch (e: Exception) { + fail() + } + } + + @Test + fun `test exception`() = runBlocking { + mockkObject(PIProtect) + val mockSlot = slot() + coEvery { + PIProtect.start(context, capture(mockSlot)) + } throws(PingOneProtectInitException("init failed")) + coEvery { + PIProtect.resumeBehavioralData() + } returns(Unit) + coEvery { + PIProtect.pauseBehavioralData() + } returns(Unit) + try { + val json = + "{\"type\":\"PingOneProtectInitializeCallback\",\"output\":[{\"name\":\"envId\",\"value\":\"02fb4743-189a-4bc7-9d6c-a919edfe6447\"},{\"name\":\"consoleLogEnabled\",\"value\":false},{\"name\":\"deviceAttributesToIgnore\",\"value\":[\"value1\", \"value2\"]},{\"name\":\"customHost\",\"value\":\"\"},{\"name\":\"lazyMetadata\",\"value\":false},{\"name\":\"behavioralDataCollection\",\"value\":false},{\"name\":\"deviceKeyRsyncIntervals\",\"value\":14},{\"name\":\"enableTrust\",\"value\":false},{\"name\":\"disableTags\",\"value\":false},{\"name\":\"disableHub\",\"value\":false}],\"input\":[{\"name\":\"IDToken1clientError\",\"value\":\"\"}]}" + val raw = JSONObject(json) + val pingOneInitCallback = PingOneProtectInitializeCallback(raw, 0) + pingOneInitCallback.start(context) + fail() + } catch (e: Exception) { + coVerify(exactly = 1) { PIProtect.start(context, any()) } + coVerify(exactly = 0) { PIProtect.resumeBehavioralData() } + coVerify(exactly = 0) { PIProtect.pauseBehavioralData() } + assertTrue(e is PingOneProtectInitException) + } + } +} \ No newline at end of file diff --git a/ping-protect/src/test/java/org/forgerock/android/auth/PingOneProviderTest.kt b/ping-protect/src/test/java/org/forgerock/android/auth/PingOneProviderTest.kt new file mode 100644 index 00000000..11fd10d5 --- /dev/null +++ b/ping-protect/src/test/java/org/forgerock/android/auth/PingOneProviderTest.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth + +import io.mockk.MockKAnnotations +import io.mockk.mockk +import io.mockk.verify +import org.forgerock.android.auth.callback.CallbackFactory +import org.junit.Before +import org.junit.Test + +class PingOneProviderTest { + + @Before + fun setUp() { + MockKAnnotations.init(this) + } + @Test + fun testPauseBehaviourDataCalled() { + + val factory = mockk(relaxed = true) + val testObject = PingOneProvider() + testObject.factory = factory + testObject.onCreate() + verify { factory.register(PingOneProtectInitializeCallback::class.java) } + verify { factory.register(PingOneProtectEvaluationCallback::class.java) } + } +} \ No newline at end of file diff --git a/ping-protect/src/test/resources/robolectric.properties b/ping-protect/src/test/resources/robolectric.properties new file mode 100644 index 00000000..78ce4557 --- /dev/null +++ b/ping-protect/src/test/resources/robolectric.properties @@ -0,0 +1,8 @@ +# +# Copyright (c) 2024 ForgeRock. All rights reserved. +# +# This software may be modified and distributed under the terms +# of the MIT license. See the LICENSE file for details. +# + +sdk=28 \ No newline at end of file diff --git a/samples/app/build.gradle b/samples/app/build.gradle deleted file mode 100644 index cf9d756f..00000000 --- a/samples/app/build.gradle +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (c) 2023 ForgeRock. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' -} - -android { - namespace 'com.example.app' - compileSdk 34 - - defaultConfig { - applicationId "com.example.app" - minSdk 23 - targetSdk 34 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled true - shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - signingConfigs { - debug { - storeFile file('../debug.jks') - storePassword 'android' - keyAlias 'androiddebugkey' - keyPassword 'android' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = '17' - } - - buildFeatures { - compose true - } - - composeOptions { - kotlinCompilerExtensionVersion = "1.4.1" - } - -} - -repositories { - maven { - url 'https://oss.sonatype.org/content/repositories/snapshots/' - } -} - -dependencies { - - def composeBom = platform('androidx.compose:compose-bom:2022.10.00') - implementation composeBom - androidTestImplementation composeBom - - //SDK - implementation project(':forgerock-auth') - //implementation 'org.forgerock:forgerock-auth:4.2.0' - //Device Binding + JWT + Application Pin - implementation 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0' //Application PIN - implementation 'androidx.security:security-crypto:1.1.0-alpha05' - implementation 'com.nimbusds:nimbus-jose-jwt:9.25' - implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05' - - //WebAuthn - implementation 'com.google.android.gms:play-services-fido:20.0.1' - - //Centralize Login - implementation 'net.openid:appauth:0.11.1' - //Captcha - implementation 'com.google.android.gms:play-services-safetynet:18.0.1' - - //Device Profile to retrieve Location - implementation "com.google.accompanist:accompanist-permissions:0.30.1" - - //Social Login - implementation 'com.google.android.gms:play-services-auth:20.6.0' - implementation 'com.facebook.android:facebook-login:16.0.0' - - //For App integrity - implementation 'com.google.android.play:integrity:1.3.0' - - //Capture Location for Device Profile - implementation 'com.google.android.gms:play-services-location:21.0.1' - - //For IG, invoke endpoint using okHttp - implementation 'com.squareup.okhttp3:okhttp:4.11.0' - implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0' - - - //End of SDK - - -// Material Design 3 - implementation 'com.google.android.material:material:1.9.0' - implementation 'androidx.compose.material3:material3' - implementation 'androidx.core:core-splashscreen:1.0.1' - - // Android Studio Preview support - implementation 'androidx.compose.ui:ui-tooling-preview' - debugImplementation 'androidx.compose.ui:ui-tooling' - - implementation 'androidx.activity:activity-compose:1.7.2' - - def nav_version = "2.7.0" - implementation("androidx.navigation:navigation-compose:$nav_version") - implementation "androidx.compose.material:material-icons-extended:1.5.0" - - implementation 'androidx.core:core-ktx:1.10.1' - implementation 'androidx.appcompat:appcompat:1.6.1' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' -} \ No newline at end of file diff --git a/samples/app/build.gradle.kts b/samples/app/build.gradle.kts new file mode 100644 index 00000000..c85ba02d --- /dev/null +++ b/samples/app/build.gradle.kts @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2023 - 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +plugins { + id("com.android.application") + id("kotlin-android") +} + +android { + namespace = "com.example.app" + compileSdk = 34 + defaultConfig { + minSdk = 23 + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildTypes { + getByName("release") { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + ("proguard-rules.pro"), + ) + } + } + signingConfigs { + getByName("debug") { + storeFile = file("../debug.jks") + storePassword = "android" + keyAlias = "androiddebugkey" + keyPassword = "android" + } + } + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.8" + } +} + +dependencies { + + val composeBom = platform("androidx.compose:compose-bom:2022.10.00") + implementation(composeBom) + + // SDK + implementation(project(":forgerock-auth")) + implementation(project(":ping-protect")) + + // implementation 'org.forgerock:forgerock-auth:4.2.0' + // Device Binding + JWT + Application Pin + implementation(libs.bcpkix.jdk15on) // Application Pin + implementation(libs.androidx.security.crypto) + implementation(libs.nimbus.jose.jwt) + implementation(libs.androidx.biometric.ktx) + + // WebAuthn + implementation(libs.play.services.fido) + + // Centralize Login + implementation(libs.appauth) + + // Captcha + implementation(libs.play.services.safetynet) + + // Social Login + implementation(libs.play.services.auth) + implementation(libs.facebook.login) + + // For App integrity + implementation(libs.integrity) + + // Capture Location for Device Profile + implementation(libs.play.services.location) + + // For IG, invoke endpoint using okHttp + implementation(libs.okhttp) + implementation(libs.logging.interceptor) + + // End of SDK + + // Keep the sample application specific library out of the toml + // Material Design 3 + implementation("com.google.android.material:material:1.11.0") + implementation("androidx.compose.material3:material3") + implementation("androidx.core:core-splashscreen:1.0.1") + + // Android Studio Preview support + implementation("androidx.compose.ui:ui-tooling-preview") + debugImplementation("androidx.compose.ui:ui-tooling") + + implementation("androidx.activity:activity-compose:1.8.2") + + implementation("androidx.navigation:navigation-compose:2.7.7") + implementation("androidx.compose.material:material-icons-extended:1.6.2") + + implementation(libs.androidx.appcompat) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.ext.junit) +} \ No newline at end of file diff --git a/samples/app/proguard-rules.pro b/samples/app/proguard-rules.pro index 481bb434..ff59496d 100644 --- a/samples/app/proguard-rules.pro +++ b/samples/app/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/samples/app/src/main/java/com/example/app/MainActivity.kt b/samples/app/src/main/java/com/example/app/MainActivity.kt index 6084a31f..406b6616 100644 --- a/samples/app/src/main/java/com/example/app/MainActivity.kt +++ b/samples/app/src/main/java/com/example/app/MainActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 ForgeRock. All rights reserved. + * Copyright (c) 2023-2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -11,10 +11,7 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import org.forgerock.android.auth.FRUserKeys class MainActivity : AppCompatActivity() { diff --git a/samples/app/src/main/java/com/example/app/callback/PingOneProtectEvaluationCallback.kt b/samples/app/src/main/java/com/example/app/callback/PingOneProtectEvaluationCallback.kt new file mode 100644 index 00000000..00b069fc --- /dev/null +++ b/samples/app/src/main/java/com/example/app/callback/PingOneProtectEvaluationCallback.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package com.example.app.callback + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import org.forgerock.android.auth.Logger +import org.forgerock.android.auth.PingOneProtectEvaluationCallback +import org.forgerock.android.auth.PingOneProtectEvaluationException + +@Composable +fun PingOneProtectEvaluationCallback(callback: PingOneProtectEvaluationCallback, onCompleted: () -> Unit) { + + val currentOnCompleted by rememberUpdatedState(onCompleted) + val context = LocalContext.current + + Column(modifier = Modifier + .padding(8.dp) + .fillMaxHeight() + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = "Collecting PingOne Signals...") + Spacer(Modifier.height(8.dp)) + CircularProgressIndicator() + + LaunchedEffect(true) { + try { + callback.getData(context) + } catch (e: PingOneProtectEvaluationException) { + Logger.error("PingOneRiskEvaluationCallback", e, e.message) + } catch (e: Exception) { + Logger.error("PingOneRiskEvaluationCallback", e, e.message) + } + currentOnCompleted() + } + } +} + diff --git a/samples/app/src/main/java/com/example/app/callback/PingOneProtectInitializeCallback.kt b/samples/app/src/main/java/com/example/app/callback/PingOneProtectInitializeCallback.kt new file mode 100644 index 00000000..7380f4ae --- /dev/null +++ b/samples/app/src/main/java/com/example/app/callback/PingOneProtectInitializeCallback.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package com.example.app.callback + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import org.forgerock.android.auth.Logger +import org.forgerock.android.auth.PingOneProtectInitializeCallback +import org.forgerock.android.auth.PingOneProtectInitException + +@Composable +fun PingOneProtectInitializeCallback( + callback: PingOneProtectInitializeCallback, + onCompleted: () -> Unit, +) { + val currentOnCompleted by rememberUpdatedState(onCompleted) + val context = LocalContext.current + + Column( + modifier = + Modifier + .padding(8.dp) + .fillMaxHeight() + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text(text = "Collecting PingOne Signals...") + Spacer(Modifier.height(8.dp)) + CircularProgressIndicator() + + LaunchedEffect(true) { + try { + callback.start(context) + } catch (e: PingOneProtectInitException) { + Logger.error("PingOneInitException", e, e.message) + } catch (e: Exception) { + Logger.error("PingOneInitException", e, e.message) + } + currentOnCompleted() + } + } +} diff --git a/samples/app/src/main/java/com/example/app/callback/TextInputCallback.kt b/samples/app/src/main/java/com/example/app/callback/TextInputCallback.kt new file mode 100644 index 00000000..cb2d2096 --- /dev/null +++ b/samples/app/src/main/java/com/example/app/callback/TextInputCallback.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +package com.example.app.callback + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.forgerock.android.auth.callback.TextInputCallback +import org.json.JSONObject + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TextInputCallback(textInputCallback: TextInputCallback) { + + var text by remember { + mutableStateOf("") + } + + Row(modifier = Modifier + .padding(4.dp) + .fillMaxWidth()) { + OutlinedTextField( + modifier = Modifier, + value = text, + onValueChange = { value -> + text = value + textInputCallback.setValue(text) + }, + label = { Text(textInputCallback.prompt) }, + ) + } + +} + +@Preview +@Composable +fun TextInputCallbackPreview() { + val json = JSONObject("{\n" + + " \"type\": \"TextInputCallback\",\n" + + " \"output\": [\n" + + " {\n" + + " \"name\": \"prompt\",\n" + + " \"value\": \"Enter text\"\n" + + " },\n" + + " {\n" + + " \"name\": \"defaultText\",\n" + + " \"value\": \"\"\n" + + " }\n" + + " ],\n" + + " \"input\": [\n" + + " {\n" + + " \"name\": \"IDToken1\",\n" + + " \"value\": \"\"\n" + + " }\n" + + " ]\n" + + "}") + TextInputCallback(TextInputCallback(json, 0)) +} \ No newline at end of file diff --git a/samples/app/src/main/java/com/example/app/centralize/CentralizeLoginViewModel.kt b/samples/app/src/main/java/com/example/app/centralize/CentralizeLoginViewModel.kt index 723fd867..62efa894 100644 --- a/samples/app/src/main/java/com/example/app/centralize/CentralizeLoginViewModel.kt +++ b/samples/app/src/main/java/com/example/app/centralize/CentralizeLoginViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 ForgeRock. All rights reserved. + * Copyright (c) 2023-2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -7,6 +7,7 @@ package com.example.app.centralize +import androidx.browser.customtabs.CustomTabsIntent import androidx.fragment.app.FragmentActivity import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -20,7 +21,11 @@ class CentralizeLoginViewModel : ViewModel() { private set fun login(fragmentActivity: FragmentActivity) { - FRUser.browser().login(fragmentActivity, + FRUser.browser().appAuthConfigurer().customTabsIntent { + it.setColorScheme(CustomTabsIntent.COLOR_SCHEME_DARK) + + }.done() + .login(fragmentActivity, object : FRListener { override fun onSuccess(result: FRUser) { state.update { diff --git a/samples/app/src/main/java/com/example/app/env/EnvViewModel.kt b/samples/app/src/main/java/com/example/app/env/EnvViewModel.kt index 7d7f1968..9810ea64 100644 --- a/samples/app/src/main/java/com/example/app/env/EnvViewModel.kt +++ b/samples/app/src/main/java/com/example/app/env/EnvViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 ForgeRock. All rights reserved. + * Copyright (c) 2023-2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -22,9 +22,9 @@ class EnvViewModel : ViewModel() { val localhost = FROptionsBuilder.build { server { - url = "http://192.168.86.248:8080/openam" - realm = "root" - cookieName = "iPlanetDirectoryPro" + url = "https://openam-protect2.forgeblocks.com/am" + realm = "alpha" + cookieName = "c1c805de4c9b333" timeout = 50 } oauth { @@ -34,8 +34,12 @@ class EnvViewModel : ViewModel() { oauthScope = "openid profile email address phone" oauthThresholdSeconds = 0 } + service { + authServiceName = "protect" + } } + val dbind = FROptionsBuilder.build { server { url = "https://openam-updbind.forgeblocks.com/am" @@ -149,13 +153,13 @@ class EnvViewModel : ViewModel() { } fun select(context: Context, host: String) { - servers.find { + servers.find { it.server.url == host }?.let { select(context, it) - } ?: run { - select(context, dbind) - } + } ?: run { + select(context, dbind) + } } fun getAll(): List { diff --git a/samples/app/src/main/java/com/example/app/journey/Journey.kt b/samples/app/src/main/java/com/example/app/journey/Journey.kt index 7d2e3e40..13aaf1b9 100644 --- a/samples/app/src/main/java/com/example/app/journey/Journey.kt +++ b/samples/app/src/main/java/com/example/app/journey/Journey.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 ForgeRock. All rights reserved. + * Copyright (c) 2023 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -31,8 +31,11 @@ import com.example.app.callback.DeviceSigningVerifierCallback import com.example.app.callback.IdPCallback import com.example.app.callback.NameCallback import com.example.app.callback.PasswordCallback +import com.example.app.callback.PingOneProtectEvaluationCallback +import com.example.app.callback.PingOneProtectInitializeCallback import com.example.app.callback.PollingWaitCallback import com.example.app.callback.SelectIdPCallback +import com.example.app.callback.TextInputCallback import com.example.app.callback.TextOutputCallback import com.example.app.callback.WebAuthnAuthenticationCallback import com.example.app.callback.WebAuthnRegistrationCallback @@ -48,7 +51,10 @@ import org.forgerock.android.auth.callback.PasswordCallback import org.forgerock.android.auth.callback.PollingWaitCallback import org.forgerock.android.auth.callback.ReCaptchaCallback import com.example.app.callback.ReCaptchaCallback +import org.forgerock.android.auth.PingOneProtectEvaluationCallback +import org.forgerock.android.auth.PingOneProtectInitializeCallback import org.forgerock.android.auth.callback.SelectIdPCallback +import org.forgerock.android.auth.callback.TextInputCallback import org.forgerock.android.auth.callback.TextOutputCallback import org.forgerock.android.auth.callback.WebAuthnAuthenticationCallback import org.forgerock.android.auth.callback.WebAuthnRegistrationCallback @@ -151,6 +157,19 @@ fun Journey(state: JourneyState, showNext = false } + is PingOneProtectEvaluationCallback -> { + PingOneProtectEvaluationCallback(callback = it, onCompleted = onNext) + showNext = false + } + + is PingOneProtectInitializeCallback -> { + PingOneProtectInitializeCallback(callback = it, onCompleted = onNext) + showNext = false + } + + is TextInputCallback -> TextInputCallback(it) + + else -> { //Unsupported } diff --git a/samples/app/src/main/java/com/example/app/setting/SettingRoute.kt b/samples/app/src/main/java/com/example/app/setting/SettingRoute.kt index 207273e3..e44814a8 100644 --- a/samples/app/src/main/java/com/example/app/setting/SettingRoute.kt +++ b/samples/app/src/main/java/com/example/app/setting/SettingRoute.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 ForgeRock. All rights reserved. + * Copyright (c) 2023 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -7,11 +7,14 @@ package com.example.app.setting +import android.content.Context import android.net.Uri +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -23,6 +26,9 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.example.app.journey.Journey import com.example.app.journey.JourneyViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.forgerock.android.auth.Action import org.forgerock.android.auth.FRRequestInterceptor import org.forgerock.android.auth.PolicyAdvice @@ -35,10 +41,17 @@ private const val BINDING = "enableBiometric" fun SettingRoute(viewModel: SettingViewModel) { val checked by viewModel.settingState.collectAsState() + val context = LocalContext.current when (checked.transitionState) { + SettingTransitionState.Disabled -> { - BiometricSetting(isChecked = false, viewModel = viewModel) + Column(modifier = Modifier + .fillMaxWidth() + ) { + BiometricSetting(isChecked = false, viewModel = viewModel) + PingProtectSetting(viewModel = viewModel, context) + } } SettingTransitionState.EnableBinding -> { @@ -70,7 +83,12 @@ fun SettingRoute(viewModel: SettingViewModel) { } SettingTransitionState.Enabled -> { - BiometricSetting(isChecked = true, viewModel = viewModel) + Column(modifier = Modifier + .fillMaxWidth() + ) { + BiometricSetting(isChecked = true, viewModel = viewModel) + PingProtectSetting(viewModel = viewModel, context) + } } else -> {} @@ -96,4 +114,28 @@ fun BiometricSetting(isChecked: Boolean, viewModel: SettingViewModel) { } ) } +} + +@Composable +fun PingProtectSetting(viewModel: SettingViewModel, context: Context) { + Row( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth()) { + + Text(text = "PingProtect Init") + + Spacer(modifier = Modifier.weight(1f, true)) + + Button( + onClick = { + val scope = CoroutineScope(Dispatchers.Main) + scope.launch { + viewModel.initViewModel(context) + } + + }) { + Text(text = "Enable") + } + } } \ No newline at end of file diff --git a/samples/app/src/main/java/com/example/app/setting/SettingViewModel.kt b/samples/app/src/main/java/com/example/app/setting/SettingViewModel.kt index c4c95124..93083798 100644 --- a/samples/app/src/main/java/com/example/app/setting/SettingViewModel.kt +++ b/samples/app/src/main/java/com/example/app/setting/SettingViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 ForgeRock. All rights reserved. + * Copyright (c) 2023 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -8,6 +8,8 @@ package com.example.app.setting import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -16,6 +18,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.forgerock.android.auth.FRUserKeys +import org.forgerock.android.auth.Logger +import org.forgerock.android.auth.PIInitParams +import org.forgerock.android.auth.PIProtect import org.forgerock.android.auth.devicebind.UserKey class SettingViewModel(context: Context) : ViewModel() { @@ -57,7 +62,21 @@ class SettingViewModel(context: Context) : ViewModel() { it.copy(transitionState = SettingTransitionState.Disabled) } } - + suspend fun initViewModel(context: Context) { + try { + val params = + PIInitParams( + envId = "02fb4743-189a-4bc7-9d6c-a919edfe6447", + isBehavioralDataCollection = false, + isConsoleLogEnabled = true, + ) + PIProtect.start(context, params) + Logger.info("Settings Protect", "Initialize succeeded") + } catch (e: Exception) { + Logger.error("Initialize Error", e.message) + throw e + } + } private fun fetch(t: Throwable?) { val state: SettingTransitionState = if (frUserKeys.loadAll().isNotEmpty()) { diff --git a/samples/auth/build.gradle b/samples/auth/build.gradle index 027b5434..94695e6b 100644 --- a/samples/auth/build.gradle +++ b/samples/auth/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -39,6 +39,10 @@ android { } } + buildFeatures { + buildConfig true + } + signingConfigs { debug { storeFile file('../debug.jks') @@ -54,13 +58,6 @@ android { } } -repositories { - maven { - url 'https://oss.sonatype.org/content/repositories/snapshots/' - - } -} - dependencies { implementation project(':forgerock-auth') @@ -93,7 +90,7 @@ dependencies { implementation 'com.google.android.play:integrity:1.3.0' //Device Binding + JWT - implementation 'com.nimbusds:nimbus-jose-jwt:9.25' + implementation 'com.nimbusds:nimbus-jose-jwt:9.37.3' implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05' //Application Pin diff --git a/samples/auth/src/main/java/org/forgerock/auth/MainActivity.java b/samples/auth/src/main/java/org/forgerock/auth/MainActivity.java index f4a76389..ec820f0e 100644 --- a/samples/auth/src/main/java/org/forgerock/auth/MainActivity.java +++ b/samples/auth/src/main/java/org/forgerock/auth/MainActivity.java @@ -28,6 +28,7 @@ import android.widget.ProgressBar; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -82,11 +83,14 @@ public class MainActivity extends AppCompatActivity { public static final int AUTH_REQUEST_CODE = 100; public static final int REQUEST_CODE = 100; private static final String TAG = MainActivity.class.getSimpleName(); + public static final String LAUNCH_BROWSER = "LAUNCH_BROWSER"; private ImageView success; private TextView content; private ProgressBar progressBar; + private boolean launchBrowser = false; + @Override protected void onCreate(Bundle savedInstanceState) { @@ -116,6 +120,20 @@ protected void onCreate(Bundle savedInstanceState) { startActivityForResult(resume, AUTH_REQUEST_CODE); } } + + if (savedInstanceState != null) { + launchBrowser = savedInstanceState.getBoolean(LAUNCH_BROWSER, launchBrowser); + if (launchBrowser) { + launchBrowser(); + } + } + + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(LAUNCH_BROWSER, launchBrowser); + super.onSaveInstanceState(outState); } @Override @@ -475,6 +493,7 @@ public void launchTree(String result) { } public void launchBrowser() { + launchBrowser = true; FRUser.browser().appAuthConfigurer() .authorizationRequest(r -> { @@ -492,11 +511,13 @@ public void launchBrowser() { .login(this, new FRListener() { @Override public void onSuccess(FRUser result) { + launchBrowser = false; userinfo(); } @Override public void onException(Exception e) { + launchBrowser = false; runOnUiThread(() -> { progressBar.setVisibility(INVISIBLE); content.setText(e.getMessage()); diff --git a/samples/authenticator/build.gradle b/samples/authenticator/build.gradle index b48b3fcd..4bb62bc5 100644 --- a/samples/authenticator/build.gradle +++ b/samples/authenticator/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2020 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -38,7 +38,8 @@ android { abortOnError false } buildFeatures { - viewBinding true + compose true + buildConfig true } } diff --git a/samples/kotlin/build.gradle b/samples/kotlin/build.gradle index d9537f35..8a84f364 100644 --- a/samples/kotlin/build.gradle +++ b/samples/kotlin/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023 ForgeRock. All rights reserved. + * Copyright (c) 2022-2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -41,6 +41,10 @@ android { } } + buildFeatures { + buildConfig true + } + flavorDimensions "environment" productFlavors { central { @@ -76,7 +80,7 @@ dependencies { implementation 'com.google.android.material:material:1.8.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.google.android.gms:play-services-fido:20.0.1' - implementation 'com.nimbusds:nimbus-jose-jwt:9.25' + implementation 'com.nimbusds:nimbus-jose-jwt:9.37.3' //Application Pin implementation 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0' diff --git a/samples/kotlin/src/main/java/com/forgerock/kotlinapp/MainActivity.kt b/samples/kotlin/src/main/java/com/forgerock/kotlinapp/MainActivity.kt index c958af6f..e87d8360 100644 --- a/samples/kotlin/src/main/java/com/forgerock/kotlinapp/MainActivity.kt +++ b/samples/kotlin/src/main/java/com/forgerock/kotlinapp/MainActivity.kt @@ -187,8 +187,8 @@ class MainActivity : AppCompatActivity(), NodeListener, ActivityListener runOnUiThread { val deviceBindingCallback = node.getCallback(DeviceBindingCallback::class.java) - deviceBindingCallback.bind(activity, listener = object : FRListener { - override fun onSuccess(result: Void) { + deviceBindingCallback.bind(activity, listener = object : FRListener { + override fun onSuccess(result: Void?) { node.next(activity, activity) } @@ -202,8 +202,8 @@ class MainActivity : AppCompatActivity(), NodeListener, ActivityListener runOnUiThread { val deviceBindingCallback = node.getCallback(DeviceSigningVerifierCallback::class.java) - deviceBindingCallback.sign(activity, listener = object : FRListener { - override fun onSuccess(result: Void) { + deviceBindingCallback.sign(activity, listener = object : FRListener { + override fun onSuccess(result: Void?) { node.next(activity, activity) } diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 7f7c6c3f..00000000 --- a/settings.gradle +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2019 ForgeRock. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -include ':forgerock-auth', ':forgerock-auth-ui', ':forgerock-core', ':forgerock-authenticator' - -include ':auth' -project(':auth').projectDir = new File('samples/auth') - -include ':authenticator' -project(':authenticator').projectDir = new File('samples/authenticator') - -/* -include ':pebblebank' -project(':pebblebank').projectDir = new File('demo/pebblebank') -*/ - -include ':quickstart' -project(':quickstart').projectDir = new File('samples/quickstart') - -include ':kotlin' -project(':kotlin').projectDir = new File('samples/kotlin') - -include ':app' -project(':app').projectDir = new File('samples/app') diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..6b033cd7 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven(url = "https://oss.sonatype.org/content/repositories/snapshots/") + } +} + +include(":forgerock-auth") +include(":forgerock-auth-ui") +include(":forgerock-core") +include(":forgerock-authenticator") +include(":ping-protect") + +include(":auth") +project(":auth").projectDir = File("samples/auth") + +include(":authenticator") +project(":authenticator").projectDir = File("samples/authenticator") + +/* +include ':pebblebank' +project(':pebblebank').projectDir = new File('demo/pebblebank') +*/ + +include(":quickstart") +project(":quickstart").projectDir = File("samples/quickstart") + +include(":kotlin") +project(":kotlin").projectDir = File("samples/kotlin") + +include(":app") +project(":app").projectDir = File("samples/app") + +include(":forgerock-integration-tests")