diff --git a/AGP_UPGRADE_PLAN.md b/AGP_UPGRADE_PLAN.md deleted file mode 100644 index a484fb121..000000000 --- a/AGP_UPGRADE_PLAN.md +++ /dev/null @@ -1,399 +0,0 @@ -# AGP and Gradle Upgrade Plan: Version 7 to Version 8+ - -## Current State Analysis - -### Current Versions -- **Gradle**: 7.5 -- **AGP (Android Gradle Plugin)**: 7.4.0 -- **Kotlin**: 1.8.22 -- **Compile SDK**: 35 -- **Target SDK**: 35 -- **Min SDK**: 21 (library), 24 (sample) -- **Java Compatibility**: VERSION_11 (library), VERSION_1_8 (sample) - -### Project Structure -- Multi-module project: `auth0` (library) + `sample` (application) -- Uses Groovy DSL for build scripts (no Kotlin DSL) -- Custom Gradle scripts: jacoco.gradle, maven-publish.gradle, versioning.gradle - -### Key Dependencies Identified -**AndroidX Libraries:** -- androidx.core:core-ktx:1.6.0 -- androidx.appcompat:appcompat:1.6.0 (library), 1.3.0 (sample) -- androidx.browser:browser:1.4.0 -- androidx.biometric:biometric:1.1.0 -- androidx.credentials:credentials:1.3.0 - -**Networking:** -- com.squareup.okhttp3:okhttp:4.12.0 -- com.squareup.okhttp3:logging-interceptor:4.12.0 -- com.google.code.gson:gson:2.8.9 - -**Coroutines:** -- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2 - -**Testing:** -- JUnit 4.13.2 -- Robolectric 4.8.1 -- PowerMock 2.0.9 -- Mockito 3.12.4 -- Espresso 3.5.1 (library), 3.4.0 (sample) - -### Critical Issues Found - -1. **gradle.properties Alert**: Contains temporary workaround - ``` - # Adding this here temporarily to fix the build with compileSdKVersion 35. Remove this when migrate to gradle 8 - android.aapt2Version=8.6.1-11315950 - ``` - This indicates the project is already encountering issues with SDK 35 on AGP 7. - -2. **Deprecated JCenter Repository**: Still using JCenter for specific dependencies - - org.jetbrains.trove4j:trove4j - - com.soywiz.korlibs.korte:korte-jvm - - org.jetbrains.kotlinx:kotlinx-html-jvm - -3. **Outdated Dependencies**: Several dependencies need updates for AGP 8 compatibility - -4. **Jacoco Configuration**: Uses deprecated `xml.enabled` / `html.enabled` syntax - -5. **Lint Options**: Uses deprecated `lintOptions` block (should be `lint`) - -6. **CI/CD**: GitHub Actions setup uses older Gradle/Kotlin versions in CI config - -## Recommended Target Versions - -### Primary Recommendations -- **Gradle**: 8.10.2 (Latest stable with excellent AGP 8.x support) -- **AGP**: 8.7.3 (Latest stable for SDK 35 - removes need for AAPT2 workaround) -- **Kotlin**: 2.0.21 (Full compatibility with AGP 8.x, K2 compiler) -- **Java Target**: Remain at Java 11 (already compliant) -- **JaCoCo**: 0.8.5 → 0.8.12 - -## Critical Breaking Changes - -### 1. PowerMock Incompatibility (SHOW STOPPER) -**Problem**: PowerMock 2.0.9 uses bytecode manipulation incompatible with Java Module System required by AGP 8.x - -**Current Dependencies (auth0/build.gradle:102-104)**: -```groovy -testImplementation "org.powermock:powermock-module-junit4:$powermockVersion" -testImplementation "org.powermock:powermock-module-junit4-rule:$powermockVersion" -testImplementation "org.powermock:powermock-api-mockito2:$powermockVersion" -``` - -**Affected Test Files** (only 2 files!): -1. `auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java` - - Mocks: KeyGenerator, TextUtils, Build.VERSION, Base64, Cipher, Log, KeyStore - - Purpose: Testing Android KeyStore cryptographic operations - - Strategy: Use Robolectric's shadow classes for Android framework mocking - -2. `auth0/src/test/java/com/auth0/android/dpop/DPoPKeyStoreTest.kt` - - Mocks: KeyStore, KeyPairGenerator, KeyGenParameterSpec.Builder, Build.VERSION, Log - - Purpose: Testing DPoP key storage and generation - - Strategy: Use Robolectric + refactor to reduce static mocking needs - -**Chosen Approach**: Remove PowerMock and refactor tests to use Robolectric + standard Mockito -- Robolectric already provides shadows for most Android framework classes (Build.VERSION, Log, TextUtils, Base64) -- KeyStore and Cipher operations can be tested with real Android KeyStore via Robolectric -- Reduces test complexity and improves compatibility - -**Impact**: 2-3 hours of test refactoring (only 2 test files affected) - -### 2. Deprecated DSL Syntax -Must update before AGP 8.x will work: -- `lintOptions` → `lint` (auth0/build.gradle:53) -- `xml.enabled` → `xml.required` (gradle/jacoco.gradle:48-49) -- `compileSdkVersion` → `compileSdk` (sample/build.gradle:7) - -### 3. JCenter Deprecation Warning -Still using JCenter for specific Dokka dependencies (trove4j, kotlinx-html-jvm). These are now on Maven Central, so repositories will continue working. - -## Step-by-Step Upgrade Sequence - -### Phase 1: Pre-Upgrade Preparation -1. **Remove AAPT2 Workaround** - - File: `gradle.properties` - - Remove: `android.aapt2Version=8.6.1-11315950` - - This was a temporary fix that AGP 8.7.3 resolves - -2. **Validate Current Build** - ```bash - ./gradlew clean build test jacocoTestReport --stacktrace - ``` - -3. **Update Gradle Wrapper** - - File: `gradle/wrapper/gradle-wrapper.properties` - - Change: `distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip` - - Run: `./gradlew wrapper --gradle-version=8.10.2 --distribution-type=all` - -4. **Update AGP Version** - - File: `build.gradle` (root) - - Line 16: `classpath 'com.android.tools.build:gradle:8.7.3'` - -### Phase 2: Fix Deprecated DSL Syntax - -5. **Fix lintOptions** (auth0/build.gradle:53-56) - ```groovy - // OLD - lintOptions { - htmlReport true - abortOnError true - } - - // NEW - lint { - htmlReport = true - abortOnError = true - } - ``` - -6. **Fix JaCoCo Reports** (gradle/jacoco.gradle:47-50) - ```groovy - // OLD - reports { - xml.enabled = true - html.enabled = true - } - - // NEW - reports { - xml.required = true - html.required = true - } - ``` - -7. **Fix SDK Version Syntax** (sample/build.gradle:7-11) - ```groovy - // OLD - compileSdkVersion 35 - minSdkVersion 24 - targetSdkVersion 35 - - // NEW - compileSdk 35 - minSdk 24 - targetSdk 35 - ``` - -### Phase 3: Kotlin Upgrade - -8. **Update Kotlin Version** - - File: `build.gradle` (root) - - Line 3: `ext.kotlin_version = "2.0.21"` - -9. **Update Kotlin Stdlib Reference** - - File: `auth0/build.gradle` - - Line 87: `"org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"` - - (Remove `-jdk8` suffix - it's now implicit) - -### Phase 4: Update Test Dependencies - -10. **Handle PowerMock Removal** (auth0/build.gradle) - - Remove lines 102-104 (PowerMock dependencies) - - Refactor 2 affected test files: - - CryptoUtilTest.java: Remove @RunWith(PowerMockRunner), @PrepareForTest, PowerMockito imports - - DPoPKeyStoreTest.kt: Remove @RunWith(PowerMockRunner), @PrepareForTest, PowerMockito usage - - Replace static mocking with Robolectric shadows and standard Mockito - -11. **Update Mockito Ecosystem** - - Line 105: `mockito-core: 3.12.4 → 5.7.0` - - Line 107: `mockito-kotlin: 2.2.0 → org.mockito.kotlin:mockito-kotlin:5.1.0` - -12. **Update Robolectric** - - Line 111: `robolectric: 4.8.1 → 4.13.1` - -13. **Update Testing Libraries** - - `androidx.test.espresso:espresso-intents: 3.5.1 → 3.6.1` - - `androidx.test.espresso:espresso-core: 3.4.0 → 3.6.1` - - `androidx.test.ext:junit: 1.1.3 → 1.2.0` - - `awaitility: 1.7.0 → 4.2.1` - -### Phase 5: Update Runtime Dependencies - -14. **Update AndroidX Libraries** - - `androidx.core:core-ktx: 1.6.0 → 1.15.0` - - `androidx.appcompat:appcompat: 1.6.0 → 1.7.0 (sample: 1.3.0 → 1.7.0)` - - `androidx.browser:browser: 1.4.0 → 1.8.0` - - `androidx.biometric:biometric: 1.1.0 → 1.2.0` - - `androidx.constraintlayout: 2.0.4 → 2.1.4` (sample) - - `androidx.navigation: 2.3.5 → 2.8.2` (sample) - - `androidx.material: 1.4.0 → 1.12.0` (sample) - -15. **Update Coroutines** - - Line 80: `coroutinesVersion = '1.6.2' → '1.7.3'` - -16. **Update Other Dependencies** - - `gson: 2.8.9 → 2.10.1` - - `okhttp: 4.12.0` (keep - already latest) - -### Phase 6: Update gradle.properties - -17. **Clean Up Properties** (gradle.properties) - - Remove: `android.aapt2Version=8.6.1-11315950` (done in Phase 1) - - Remove: `android.enableJetifier=false` (not needed with AGP 8.x) - - Keep: `android.useAndroidX=true` - - Keep: `kotlin.code.style=official` - - Optional Add: `org.gradle.caching=true` - -### Phase 7: Update CI/CD Configuration - -18. **Update GitHub Actions** (.github/actions/setup/action.yml) - - Line 12: Default Gradle: `6.7.1 → 8.10.2` - - Line 16: Default Kotlin: `1.6.21 → 2.0.21` - -### Phase 8: Update JaCoCo Version - -19. **Update JaCoCo** (gradle/jacoco.gradle:4) - - `toolVersion = "0.8.5" → "0.8.12"` - -## Complete Dependency Update Matrix - -``` -GRADLE ECOSYSTEM: -├─ Gradle: 7.5 → 8.10.2 -├─ AGP: 7.4.0 → 8.7.3 -├─ Kotlin: 1.8.22 → 2.0.21 -├─ Java: 11 (no change) -└─ JaCoCo: 0.8.5 → 0.8.12 - -KOTLIN ECOSYSTEM: -├─ kotlin-stdlib-jdk8 → kotlin-stdlib: 2.0.21 -├─ kotlinx-coroutines: 1.6.2 → 1.7.3 - -ANDROIDX LIBRARIES: -├─ core-ktx: 1.6.0 → 1.15.0 -├─ appcompat: 1.6.0/1.3.0 → 1.7.0 -├─ browser: 1.4.0 → 1.8.0 -├─ biometric: 1.1.0 → 1.2.0 -├─ credentials: 1.3.0 (keep) -├─ constraintlayout: 2.0.4 → 2.1.4 -├─ navigation: 2.3.5 → 2.8.2 -└─ material: 1.4.0 → 1.12.0 - -TEST FRAMEWORKS: -├─ Robolectric: 4.8.1 → 4.13.1 -├─ Mockito: 3.12.4 → 5.7.0 -├─ mockito-kotlin: 2.2.0 → 5.1.0 -├─ PowerMock: 2.0.9 → REMOVE -├─ MockK: NEW 1.13.14 (optional) -├─ espresso: 3.5.1/3.4.0 → 3.6.1 -├─ awaitility: 1.7.0 → 4.2.1 -└─ androidx.test.ext:junit: 1.1.3 → 1.2.0 - -NETWORK/JSON: -├─ okhttp: 4.12.0 (keep) -└─ gson: 2.8.9 → 2.10.1 -``` - -## Testing Strategy - -### Verification Steps -```bash -# 1. Basic compilation -./gradlew clean build -x test - -# 2. Unit tests -./gradlew test --stacktrace - -# 3. Coverage reports -./gradlew test jacocoTestReport --stacktrace - -# 4. Lint checks -./gradlew lint --stacktrace - -# 5. Sample app build -./gradlew :sample:build - -# 6. Library packaging -./gradlew :auth0:assembleRelease - -# 7. CI replication -./gradlew clean test jacocoTestReport lint --continue --console=plain --max-workers=1 --no-daemon - -# 8. Maven publish dry-run -./gradlew publish -x signReleasePublication --dry-run -``` - -## Rollback Plan - -### Full Revert (if critical issues arise) -```bash -git checkout build.gradle gradle.properties gradle/wrapper/gradle-wrapper.properties -./gradlew wrapper --gradle-version=7.5 -``` - -### Partial Revert -- Upgrade only to Gradle 7.6 (without AGP 8.x) -- Provides some improvements while maintaining compatibility - -## Estimated Effort -- **Total Time**: 7-9 hours -- **Critical Path**: 5 hours minimum -- **PowerMock Refactoring**: 2-4 hours (50% of total effort) -- **Risk Level**: Medium-High (PowerMock compatibility is main blocker) - -## Recommended Implementation Order - -Based on user preferences (latest stable versions, direct Kotlin 2.0.21 upgrade, PowerMock removal): - -### Commit 1: Phase 1 - Pre-upgrade preparation -- Remove AAPT2 workaround from gradle.properties -- Validate current build passes -- Create feature branch: `git checkout -b gradle-agp-8-upgrade` - -### Commit 2: Phase 1 - Gradle wrapper upgrade -- Update gradle-wrapper.properties to 8.10.2 -- Run: `./gradlew wrapper --gradle-version=8.10.2 --distribution-type=all` -- Verify: `./gradlew --version` - -### Commit 3: Phase 1 & 2 - AGP + DSL fixes -- Update AGP to 8.7.3 in root build.gradle -- Fix lintOptions → lint (auth0/build.gradle) -- Fix JaCoCo reports syntax (gradle/jacoco.gradle) -- Fix SDK version syntax (sample/build.gradle) -- Test: `./gradlew clean build -x test` (should compile) - -### Commit 4: Phase 3 - Kotlin upgrade -- Update Kotlin to 2.0.21 in root build.gradle -- Update stdlib reference in auth0/build.gradle -- Test: `./gradlew clean build -x test` - -### Commit 5: Phase 4 - PowerMock removal & test refactoring -- Remove PowerMock dependencies from auth0/build.gradle -- Refactor CryptoUtilTest.java to use Robolectric -- Refactor DPoPKeyStoreTest.kt to use Robolectric -- Update Mockito to 5.7.0 -- Update mockito-kotlin to 5.1.0 -- Test: `./gradlew test --stacktrace` (critical milestone) - -### Commit 6: Phase 4 & 5 - Dependency updates -- Update Robolectric to 4.13.1 -- Update all AndroidX libraries -- Update coroutines to 1.7.3 -- Update espresso, awaitility, gson -- Test: `./gradlew test jacocoTestReport` - -### Commit 7: Phase 6 & 8 - Properties and tooling -- Clean up gradle.properties -- Update JaCoCo to 0.8.12 -- Update CI configuration (.github/actions/setup/action.yml) -- Test: Full CI command locally - -### Commit 8: Final verification -- Run: `./gradlew clean test jacocoTestReport lint --continue --console=plain --max-workers=1 --no-daemon` -- Verify sample app builds -- Verify library packaging -- Maven publish dry-run -- Ready for PR - -## Critical Files to Modify -- `/Users/prince.mathew/workspace/Auth0.Android/build.gradle` - AGP, Kotlin versions -- `/Users/prince.mathew/workspace/Auth0.Android/auth0/build.gradle` - DSL syntax, dependencies, PowerMock removal -- `/Users/prince.mathew/workspace/Auth0.Android/sample/build.gradle` - DSL syntax, dependencies -- `/Users/prince.mathew/workspace/Auth0.Android/gradle/wrapper/gradle-wrapper.properties` - Gradle version -- `/Users/prince.mathew/workspace/Auth0.Android/gradle/jacoco.gradle` - JaCoCo DSL syntax, version -- `/Users/prince.mathew/workspace/Auth0.Android/gradle.properties` - Property cleanup -- `/Users/prince.mathew/workspace/Auth0.Android/.github/actions/setup/action.yml` - CI configuration -- `/Users/prince.mathew/workspace/Auth0.Android/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java` - PowerMock refactoring -- `/Users/prince.mathew/workspace/Auth0.Android/auth0/src/test/java/com/auth0/android/dpop/DPoPKeyStoreTest.kt` - PowerMock refactoring diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md index 7cbe87f10..6256f7be5 100644 --- a/V4_MIGRATION_GUIDE.md +++ b/V4_MIGRATION_GUIDE.md @@ -60,7 +60,12 @@ buildscript { ## Breaking Changes -No breaking API changes have been identified in v4. This section will be updated if any are discovered. +### Classes Removed + +- The `com.auth0.android.provider.PasskeyAuthProvider` class has been removed. Use the APIs from the [AuthenticationAPIClient](auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt) class for passkey operations: + - [passkeyChallenge()](auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt#L366-L387) - Request a challenge to initiate passkey login flow + - [signinWithPasskey()](auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt#L235-L253) - Sign in a user using passkeys + - [signupWithPasskey()](auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt#L319-L344) - Sign up a user and returns a challenge for key generation ## Getting Help diff --git a/auth0/build.gradle b/auth0/build.gradle index 9c0190136..8c0d4650b 100644 --- a/auth0/build.gradle +++ b/auth0/build.gradle @@ -52,7 +52,7 @@ android { manifestPlaceholders = [auth0Domain: '${auth0Domain}', auth0Scheme: '${auth0Scheme}'] - consumerProguardFiles '../proguard/proguard-gson.pro', '../proguard/proguard-okio.pro', '../proguard/proguard-jetpack.pro' + consumerProguardFiles '../proguard/proguard-gson.pro', '../proguard/proguard-okio.pro' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lint { @@ -83,7 +83,6 @@ ext { okhttpVersion = '4.12.0' coroutinesVersion = '1.7.3' biometricLibraryVersion = '1.1.0' - credentialManagerVersion = "1.3.0" } @@ -113,9 +112,6 @@ dependencies { testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" testImplementation "androidx.biometric:biometric:$biometricLibraryVersion" - - implementation "androidx.credentials:credentials-play-services-auth:$credentialManagerVersion" - implementation "androidx.credentials:credentials:$credentialManagerVersion" } apply from: rootProject.file('gradle/jacoco.gradle') diff --git a/auth0/src/main/java/com/auth0/android/provider/PasskeyAuthProvider.kt b/auth0/src/main/java/com/auth0/android/provider/PasskeyAuthProvider.kt deleted file mode 100644 index aee248929..000000000 --- a/auth0/src/main/java/com/auth0/android/provider/PasskeyAuthProvider.kt +++ /dev/null @@ -1,234 +0,0 @@ -package com.auth0.android.provider - -import android.content.Context -import android.os.Build -import android.util.Log -import androidx.credentials.CredentialManager -import com.auth0.android.Auth0 -import com.auth0.android.authentication.AuthenticationAPIClient -import com.auth0.android.authentication.AuthenticationException -import com.auth0.android.authentication.ParameterBuilder -import com.auth0.android.callback.Callback -import com.auth0.android.request.UserData -import com.auth0.android.result.Credentials -import java.util.concurrent.Executor -import java.util.concurrent.Executors - -/** - * Passkey authentication provider - */ - -@Deprecated( - """PasskeyAuthProvider is deprecated and will be removed in the next major version of the SDK. - Use API's in [AuthenticationAPIClient] directly to support sign-in/signup with passkeys.""", - level = DeprecationLevel.WARNING -) -public object PasskeyAuthProvider { - - private val TAG = PasskeyManager::class.simpleName - - /** - * Initialize the PasskeyAuthProvider instance for signing up a user . Additional settings can be configured in the - * SignupBuilder. - * - * @param auth0 [Auth0] instance to be used for authentication - * @return a new builder instance to customize - */ - @JvmStatic - public fun signUp(auth0: Auth0): SignupBuilder { - return SignupBuilder(auth0) - } - - /** - * Initialize the PasskeyAuthProvider instance for signing in a user. Additional settings can be configured in the - * SignInBuilder - * - * @param auth0 [Auth0] instance to be used for authentication - * @return a new builder instance to customize - */ - @JvmStatic - public fun signIn(auth0: Auth0): SignInBuilder { - return SignInBuilder(auth0) - } - - - public class SignInBuilder internal constructor(private val auth0: Auth0) { - private val parameters: MutableMap = mutableMapOf() - - /** - * Specify the scope for this request. - * - * @param scope to request - * @return the current builder instance - */ - public fun setScope(scope: String): SignInBuilder = apply { - parameters[ParameterBuilder.SCOPE_KEY] = scope - } - - /** - * Specify the custom audience for this request. - * - * @param audience to use in this request - * @return the current builder instance - */ - public fun setAudience(audience: String): SignInBuilder = apply { - parameters[ParameterBuilder.AUDIENCE_KEY] = audience - } - - /** - * Specify the realm for this request - * - * @param realm to use in this request - * @return the current builder instance - */ - public fun setRealm(realm: String): SignInBuilder = apply { - parameters[ParameterBuilder.REALM_KEY] = realm - } - - /** - * Request user authentication using passkey. The result will be received in the callback. - * - * @param context context to run the authentication - * @param callback to receive the result - * @param executor optional executor to run the public key credential response creation - */ - public fun start( - context: Context, - callback: Callback, - executor: Executor = Executors.newSingleThreadExecutor() - ) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - Log.w(TAG, "Requires Android 9 or higher to use passkey authentication ") - val ex = AuthenticationException( - "Requires Android 9 or higher" - ) - callback.onFailure(ex) - return - } - val passkeyManager = PasskeyManager( - AuthenticationAPIClient(auth0), CredentialManager.create(context) - ) - passkeyManager.signin( - context, parameters[ParameterBuilder.REALM_KEY], parameters, callback, executor - ) - } - } - - - public class SignupBuilder internal constructor(private val auth0: Auth0) { - private var username: String? = null - private var email: String? = null - private var name: String? = null - private var phoneNumber: String? = null - - private val parameters: MutableMap = mutableMapOf() - - /** - * Specify the realm for this request - * - * @param realm to use in this request - * @return the current builder instance - */ - public fun setRealm(realm: String): SignupBuilder = apply { - parameters[ParameterBuilder.REALM_KEY] = realm - } - - /** - * Specify the email for the user. - * Email can be optional,required or forbidden depending on the attribute configuration for the database - * - * @param email to be set - * @return the current builder instance - */ - public fun setEmail(email: String): SignupBuilder = apply { - this.email = email - } - - /** - * Specify the username for the user. - * Username can be optional,required or forbidden depending on the attribute configuration for the database - * - * @param username to be set - * @return the current builder instance - */ - public fun setUserName(username: String): SignupBuilder = apply { - this.username = username - } - - /** - * Specify the name for the user. - * Name can be optional,required or forbidden depending on the attribute configuration for the database - * - * @param name to be set - * @return the current builder instance - */ - public fun setName(name: String): SignupBuilder = apply { - this.name = name - } - - /** - * Specify the phone number for the user - * Phone number can be optional,required or forbidden depending on the attribute configuration for the database - * - * @param number to be set - * @return the current builder instance - */ - public fun setPhoneNumber(number: String): SignupBuilder = apply { - this.phoneNumber = number - } - - /** - * Specify the scope for this request. - * - * @param scope to request - * @return the current builder instance - */ - public fun setScope(scope: String): SignupBuilder = apply { - parameters[ParameterBuilder.SCOPE_KEY] = scope - } - - /** - * Specify the custom audience for this request. - * - * @param audience to use in this request - * @return the current builder instance - */ - public fun setAudience(audience: String): SignupBuilder = apply { - parameters[ParameterBuilder.AUDIENCE_KEY] = audience - } - - /** - * Request user signup and authentication using passkey. The result will be received in the callback. - * - * @param context context to run the authentication - * @param callback to receive the result - * @param executor optional executor to run the public key credential response creation - */ - public fun start( - context: Context, - callback: Callback, - executor: Executor = Executors.newSingleThreadExecutor() - ) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - Log.w(TAG, "Requires Android 9 or higher to use passkey authentication ") - val ex = AuthenticationException( - "Requires Android 9 or higher" - ) - callback.onFailure(ex) - return - } - val passkeyManager = PasskeyManager( - AuthenticationAPIClient(auth0), CredentialManager.create(context) - ) - val userData = UserData(email, phoneNumber, username, name) - passkeyManager.signup( - context, - userData, - parameters[ParameterBuilder.REALM_KEY], - parameters, - callback, - executor - ) - } - } -} \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/provider/PasskeyManager.kt b/auth0/src/main/java/com/auth0/android/provider/PasskeyManager.kt deleted file mode 100644 index a6fc2fe99..000000000 --- a/auth0/src/main/java/com/auth0/android/provider/PasskeyManager.kt +++ /dev/null @@ -1,245 +0,0 @@ -package com.auth0.android.provider - -import android.annotation.SuppressLint -import android.content.Context -import android.os.Build -import android.os.CancellationSignal -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.credentials.CreateCredentialResponse -import androidx.credentials.CreatePublicKeyCredentialRequest -import androidx.credentials.CreatePublicKeyCredentialResponse -import androidx.credentials.CredentialManager -import androidx.credentials.CredentialManagerCallback -import androidx.credentials.GetCredentialRequest -import androidx.credentials.GetCredentialResponse -import androidx.credentials.GetPublicKeyCredentialOption -import androidx.credentials.PublicKeyCredential -import androidx.credentials.exceptions.CreateCredentialCancellationException -import androidx.credentials.exceptions.CreateCredentialException -import androidx.credentials.exceptions.CreateCredentialInterruptedException -import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException -import androidx.credentials.exceptions.GetCredentialCancellationException -import androidx.credentials.exceptions.GetCredentialException -import androidx.credentials.exceptions.GetCredentialInterruptedException -import androidx.credentials.exceptions.GetCredentialUnsupportedException -import androidx.credentials.exceptions.NoCredentialException -import com.auth0.android.authentication.AuthenticationAPIClient -import com.auth0.android.authentication.AuthenticationException -import com.auth0.android.callback.Callback -import com.auth0.android.request.PublicKeyCredentials -import com.auth0.android.request.UserData -import com.auth0.android.result.Credentials -import com.auth0.android.result.PasskeyChallenge -import com.auth0.android.result.PasskeyRegistrationChallenge -import com.google.gson.Gson -import java.util.concurrent.Executor -import java.util.concurrent.Executors - - -internal class PasskeyManager( - private val authenticationAPIClient: AuthenticationAPIClient, - private val credentialManager: CredentialManager -) { - - private val TAG = PasskeyManager::class.simpleName - - @RequiresApi(api = Build.VERSION_CODES.P) - @SuppressLint("PublicKeyCredential") - fun signup( - context: Context, - userData: UserData, - realm: String?, - parameters: Map, - callback: Callback, - executor: Executor = Executors.newSingleThreadExecutor() - ) { - authenticationAPIClient.signupWithPasskey(userData, realm) - .addParameters(parameters) - .start(object : Callback { - override fun onSuccess(result: PasskeyRegistrationChallenge) { - val pasKeyRegistrationResponse = result - val request = CreatePublicKeyCredentialRequest( - Gson().toJson( - pasKeyRegistrationResponse.authParamsPublicKey - ) - ) - var response: CreatePublicKeyCredentialResponse? - - credentialManager.createCredentialAsync(context, - request, - CancellationSignal(), - executor, - object : - CredentialManagerCallback { - - override fun onError(e: CreateCredentialException) { - Log.w(TAG, "Error while creating passkey") - callback.onFailure(handleCreationFailure(e)) - } - - override fun onResult(result: CreateCredentialResponse) { - - response = result as CreatePublicKeyCredentialResponse - val authRequest = Gson().fromJson( - response?.registrationResponseJson, - PublicKeyCredentials::class.java - ) - - authenticationAPIClient.signinWithPasskey( - pasKeyRegistrationResponse.authSession, - authRequest, - realm - ) - .validateClaims() - .addParameters(parameters) - .start(callback) - } - }) - - } - - override fun onFailure(error: AuthenticationException) { - callback.onFailure(error) - } - }) - - } - - - @RequiresApi(api = Build.VERSION_CODES.P) - fun signin( - context: Context, - realm: String?, - parameters: Map, - callback: Callback, - executor: Executor = Executors.newSingleThreadExecutor() - ) { - authenticationAPIClient.passkeyChallenge(realm) - .start(object : Callback { - override fun onSuccess(result: PasskeyChallenge) { - val passkeyChallengeResponse = result - val request = - GetPublicKeyCredentialOption(Gson().toJson(passkeyChallengeResponse.authParamsPublicKey)) - val getCredRequest = GetCredentialRequest( - listOf(request) - ) - credentialManager.getCredentialAsync(context, - getCredRequest, - CancellationSignal(), - executor, - object : - CredentialManagerCallback { - override fun onError(e: GetCredentialException) { - Log.w(TAG, "Error while fetching public key credential") - callback.onFailure(handleGetCredentialFailure(e)) - } - - override fun onResult(result: GetCredentialResponse) { - when (val credential = result.credential) { - is PublicKeyCredential -> { - val authRequest = Gson().fromJson( - credential.authenticationResponseJson, - PublicKeyCredentials::class.java - ) - authenticationAPIClient.signinWithPasskey( - passkeyChallengeResponse.authSession, - authRequest, - realm - ) - .validateClaims() - .addParameters(parameters) - .start(callback) - } - - else -> { - Log.w( - TAG, - "Received unrecognized credential type ${credential.type}.This shouldn't happen" - ) - callback.onFailure(AuthenticationException("Received unrecognized credential type ${credential.type}")) - } - } - } - }) - - } - - override fun onFailure(error: AuthenticationException) { - callback.onFailure(error) - } - }) - - } - - private fun handleCreationFailure(exception: CreateCredentialException): AuthenticationException { - return when (exception) { - - is CreateCredentialCancellationException -> { - AuthenticationException( - AuthenticationException.ERROR_VALUE_AUTHENTICATION_CANCELED, - "The user cancelled passkey authentication operation." - ) - } - - is CreateCredentialInterruptedException -> { - AuthenticationException( - "Passkey authentication was interrupted. Please retry the call." - ) - } - - is CreateCredentialProviderConfigurationException -> { - AuthenticationException( - "Provider configuration dependency is missing. Ensure credentials-play-services-auth dependency is added." - ) - } - - else -> { - Log.w(TAG, "Unexpected exception type ${exception::class.java.name}") - AuthenticationException( - "An error occurred when trying to authenticate with passkey" - ) - } - } - } - - private fun handleGetCredentialFailure(exception: GetCredentialException): AuthenticationException { - - return when (exception) { - is GetCredentialCancellationException -> { - AuthenticationException( - AuthenticationException.ERROR_VALUE_AUTHENTICATION_CANCELED, - "The user cancelled passkey authentication operation." - ) - } - - is GetCredentialInterruptedException -> { - AuthenticationException( - "Passkey authentication was interrupted. Please retry the call." - ) - } - - is GetCredentialUnsupportedException -> { - AuthenticationException( - "Credential manager is unsupported. Please update the device." - ) - } - - - is NoCredentialException -> { - AuthenticationException( - "No viable credential is available for the user" - ) - } - - - else -> { - Log.w(TAG, "Unexpected exception type ${exception::class.java.name}") - AuthenticationException( - "An error occurred when trying to authenticate with passkey" - ) - } - } - } - -} \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/provider/PasskeyManagerTest.kt b/auth0/src/test/java/com/auth0/android/provider/PasskeyManagerTest.kt deleted file mode 100644 index f53dc7455..000000000 --- a/auth0/src/test/java/com/auth0/android/provider/PasskeyManagerTest.kt +++ /dev/null @@ -1,423 +0,0 @@ -package com.auth0.android.provider - -import android.content.Context -import androidx.credentials.CreateCredentialResponse -import androidx.credentials.CreatePublicKeyCredentialResponse -import androidx.credentials.CredentialManager -import androidx.credentials.CredentialManagerCallback -import androidx.credentials.GetCredentialRequest -import androidx.credentials.GetCredentialResponse -import androidx.credentials.PublicKeyCredential -import androidx.credentials.exceptions.CreateCredentialException -import androidx.credentials.exceptions.CreateCredentialInterruptedException -import androidx.credentials.exceptions.GetCredentialException -import androidx.credentials.exceptions.GetCredentialInterruptedException -import com.auth0.android.authentication.AuthenticationAPIClient -import com.auth0.android.authentication.AuthenticationException -import com.auth0.android.authentication.request.AuthenticationRequestMock -import com.auth0.android.authentication.request.RequestMock -import com.auth0.android.callback.Callback -import com.auth0.android.request.PublicKeyCredentials -import com.auth0.android.request.UserData -import com.auth0.android.result.AuthParamsPublicKey -import com.auth0.android.result.AuthenticatorSelection -import com.auth0.android.result.AuthnParamsPublicKey -import com.auth0.android.result.Credentials -import com.auth0.android.result.PasskeyChallenge -import com.auth0.android.result.PasskeyRegistrationChallenge -import com.auth0.android.result.PasskeyUser -import com.auth0.android.result.PubKeyCredParam -import com.auth0.android.result.RelyingParty -import org.mockito.kotlin.KArgumentCaptor -import org.mockito.kotlin.any -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import org.junit.Assert -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations -import org.robolectric.RobolectricTestRunner -import java.util.Date -import java.util.concurrent.Executor - - -@RunWith(RobolectricTestRunner::class) -public class PasskeyManagerTest { - - private lateinit var passkeyManager: PasskeyManager - - @Mock - private lateinit var callback: Callback - - @Mock - private lateinit var authenticationAPIClient: AuthenticationAPIClient - - @Mock - private lateinit var credentialManager: CredentialManager - - @Mock - private lateinit var context: Context - - private val serialExecutor = Executor { runnable -> runnable.run() } - - private val credentialsCaptor: KArgumentCaptor = argumentCaptor() - private val exceptionCaptor: KArgumentCaptor = argumentCaptor() - - - private val passkeyRegistrationChallengeResponse = PasskeyRegistrationChallenge( - authSession = "dummyAuthSession", - authParamsPublicKey = AuthnParamsPublicKey( - authenticatorSelection = AuthenticatorSelection( - residentKey = "required", - userVerification = "preferred" - ), - challenge = "dummyChallenge", - pubKeyCredParams = listOf( - PubKeyCredParam( - alg = -7, - type = "public-key" - ) - ), - relyingParty = RelyingParty( - id = "dummyRpId", - name = "dummyRpName" - ), - timeout = 60000L, - user = PasskeyUser( - displayName = "displayName", - id = "userId", - name = "userName" - ) - ) - ) - - private val registrationResponseJSON = """ - { - "id": "id", - "rawId": "rawId", - "response": { - "attestationObject": "attnObject", - "clientDataJSON": "dataJSON" - }, - "type": "public-key" - } - """ - - private val passkeyChallenge = PasskeyChallenge( - authSession = "authSession", - authParamsPublicKey = AuthParamsPublicKey( - challenge = "challenge", - rpId = "RpId", - timeout = 60000, - userVerification = "preferred" - ) - ) - - @Before - public fun setUp() { - MockitoAnnotations.openMocks(this) - passkeyManager = PasskeyManager(authenticationAPIClient, credentialManager) - } - - - @Test - public fun shouldSignUpWithPasskeySuccess() { - val userMetadata: UserData = mock() - val parameters = mapOf("scope" to "profile") - - `when`(authenticationAPIClient.signupWithPasskey(userMetadata, "testRealm")).thenReturn( - RequestMock(passkeyRegistrationChallengeResponse, null) - ) - `when`( - authenticationAPIClient.signinWithPasskey( - any(), - any(), - any(), - eq(null) - ) - ).thenReturn( - AuthenticationRequestMock( - Credentials( - "expectedIdToken", - "codeAccess", - "codeType", - "codeRefresh", - Date(), - "codeScope" - ), null - ) - ) - - val createResponse: CreatePublicKeyCredentialResponse = mock() - `when`(createResponse.registrationResponseJson).thenReturn( - registrationResponseJSON - ) - - whenever( - credentialManager.createCredentialAsync( - any(), - any(), - any(), - any(), - any() - ) - ).thenAnswer { - (it.arguments[4] as CredentialManagerCallback).onResult( - createResponse - ) - } - - passkeyManager.signup( - context, - userMetadata, - "testRealm", - parameters, - callback, - serialExecutor - ) - - verify(authenticationAPIClient).signupWithPasskey(userMetadata, "testRealm") - verify(credentialManager).createCredentialAsync(eq(context), any(), any(), any(), any()) - verify(authenticationAPIClient).signinWithPasskey( - any(), any(), any(), - eq(null) - ) - verify(callback).onSuccess(credentialsCaptor.capture()) - Assert.assertEquals("codeAccess", credentialsCaptor.firstValue.accessToken) - Assert.assertEquals("codeScope", credentialsCaptor.firstValue.scope) - - } - - @Test - public fun shouldSignUpWithPasskeyApiFailure() { - val userMetadata: UserData = mock() - val parameters = mapOf("scope" to "profile") - val error = AuthenticationException("Signup failed") - `when`( - authenticationAPIClient.signupWithPasskey( - userMetadata, - "testRealm" - ) - ).thenReturn(RequestMock(null, error)) - passkeyManager.signup( - context, - userMetadata, - "testRealm", - parameters, - callback, - serialExecutor - ) - verify(authenticationAPIClient).signupWithPasskey(userMetadata, "testRealm") - verify(authenticationAPIClient, never()).signinWithPasskey( - any(), - any(), - any(), - eq(null) - ) - verify(credentialManager, never()).createCredentialAsync( - any(), - any(), - any(), - any(), - any() - ) - verify(callback).onFailure(error) - } - - @Test - public fun shouldSignUpWithPasskeyCreateCredentialFailure() { - val userMetadata: UserData = mock() - val parameters = mapOf("scope" to "scope") - `when`( - authenticationAPIClient.signupWithPasskey( - userMetadata, - "testRealm" - ) - ).thenReturn(RequestMock(passkeyRegistrationChallengeResponse, null)) - - whenever( - credentialManager.createCredentialAsync( - any(), - any(), - any(), - any(), - any() - ) - ).thenAnswer { - (it.arguments[4] as CredentialManagerCallback).onError( - CreateCredentialInterruptedException() - ) - } - - passkeyManager.signup( - context, - userMetadata, - "testRealm", - parameters, - callback, - serialExecutor - ) - verify(authenticationAPIClient).signupWithPasskey(userMetadata, "testRealm") - verify(credentialManager).createCredentialAsync(eq(context), any(), any(), any(), any()) - verify(authenticationAPIClient, never()).signinWithPasskey( - any(), - any(), - any(), eq(null) - ) - verify(callback).onFailure(exceptionCaptor.capture()) - Assert.assertEquals( - AuthenticationException::class.java, - exceptionCaptor.firstValue.javaClass - ) - Assert.assertEquals( - "Passkey authentication was interrupted. Please retry the call.", - exceptionCaptor.firstValue.message - ) - } - - - @Test - public fun shouldSignInWithPasskeySuccess() { - val parameters = mapOf("scope" to "scope") - val credentialResponse: GetCredentialResponse = mock() - - `when`(authenticationAPIClient.passkeyChallenge("testRealm")).thenReturn( - RequestMock(passkeyChallenge, null) - ) - - `when`(credentialResponse.credential).thenReturn( - PublicKeyCredential(registrationResponseJSON) - ) - - `when`( - authenticationAPIClient.signinWithPasskey( - any(), - any(), - any(), - eq(null) - ) - ).thenReturn( - AuthenticationRequestMock( - Credentials( - "expectedIdToken", - "codeAccess", - "codeType", - "codeRefresh", - Date(), - "codeScope" - ), null - ) - ) - - doAnswer { - val callback = - it.getArgument>( - 4 - ) - callback.onResult(credentialResponse) - }.`when`(credentialManager) - .getCredentialAsync(any(), any(), any(), any(), any()) - - passkeyManager.signin(context, "testRealm", parameters, callback, serialExecutor) - - verify(authenticationAPIClient).passkeyChallenge("testRealm") - verify(credentialManager).getCredentialAsync( - any(), - any(), - any(), - any(), - any() - ) - verify(authenticationAPIClient).signinWithPasskey( - any(), any(), any(), - eq(null) - ) - verify(callback).onSuccess(credentialsCaptor.capture()) - Assert.assertEquals("codeAccess", credentialsCaptor.firstValue.accessToken) - Assert.assertEquals("codeScope", credentialsCaptor.firstValue.scope) - } - - - @Test - public fun shouldSignInWithPasskeyApiFailure() { - val parameters = mapOf("scope" to "profile") - val error = AuthenticationException("Signin failed") - - `when`(authenticationAPIClient.passkeyChallenge("testRealm")).thenReturn( - RequestMock(null, error) - ) - - passkeyManager.signin(context, "testRealm", parameters, callback, serialExecutor) - - verify(authenticationAPIClient).passkeyChallenge(any(), eq(null)) - verify(credentialManager, never()).getCredentialAsync( - any(), - any(), - any(), - any(), - any() - ) - verify(authenticationAPIClient, never()).signinWithPasskey( - any(), - any(), - any(), - eq(null) - ) - verify(callback).onFailure(error) - } - - @Test - public fun shouldSignInWithPasskeyGetCredentialFailure() { - val parameters = mapOf("realm" to "testRealm") - `when`(authenticationAPIClient.passkeyChallenge("testRealm")).thenReturn( - RequestMock(passkeyChallenge, null) - ) - - whenever( - credentialManager.getCredentialAsync( - any(), - any(), - any(), - any(), - any() - ) - ).thenAnswer { - (it.arguments[4] as CredentialManagerCallback).onError( - GetCredentialInterruptedException() - ) - } - - passkeyManager.signin(context, "testRealm", parameters, callback, serialExecutor) - verify(authenticationAPIClient).passkeyChallenge("testRealm") - verify(credentialManager).getCredentialAsync( - any(), - any(), - any(), - any(), - any() - ) - verify(authenticationAPIClient, never()).signinWithPasskey( - any(), - any(), - any(), - eq(null) - ) - verify(callback).onFailure(exceptionCaptor.capture()) - Assert.assertEquals( - AuthenticationException::class.java, - exceptionCaptor.firstValue.javaClass - ) - Assert.assertEquals( - "Passkey authentication was interrupted. Please retry the call.", - exceptionCaptor.firstValue.message - ) - } -} diff --git a/proguard/proguard-jetpack.pro b/proguard/proguard-jetpack.pro deleted file mode 100644 index 6254a5381..000000000 --- a/proguard/proguard-jetpack.pro +++ /dev/null @@ -1,6 +0,0 @@ -# Jetpack libraries - --if class androidx.credentials.CredentialManager --keep class androidx.credentials.playservices.** { - *; -} \ No newline at end of file diff --git a/sample/build.gradle b/sample/build.gradle index d239bf435..c1246a842 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -53,10 +53,10 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' - implementation "androidx.credentials:credentials-play-services-auth:1.3.0" - implementation "androidx.credentials:credentials:1.3.0" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' implementation 'com.google.code.gson:gson:2.8.9' + implementation "androidx.credentials:credentials-play-services-auth:1.3.0" + implementation "androidx.credentials:credentials:1.3.0" } \ No newline at end of file