From ec4c6348fbb4008529e4b2d7612ae67a68362c24 Mon Sep 17 00:00:00 2001 From: Prince Mathew <17837162+pmathew92@users.noreply.github.com> Date: Thu, 26 Feb 2026 14:43:26 +0530 Subject: [PATCH 01/28] doc: Removed EA tag from N2W APIs (#920) --- EXAMPLES.md | 8 ++----- .../authentication/AuthenticationAPIClient.kt | 6 ----- .../storage/CredentialsManager.kt | 20 ---------------- .../storage/SecureCredentialsManager.kt | 23 ------------------- 4 files changed, 2 insertions(+), 55 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index cae42b8ca..1ad204be4 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -29,7 +29,7 @@ - [Sign Up with a database connection](#sign-up-with-a-database-connection) - [Get user information](#get-user-information) - [Custom Token Exchange](#custom-token-exchange) - - [Native to Web SSO login [EA]](#native-to-web-sso-login-ea) + - [Native to Web SSO login](#native-to-web-sso-login) - [DPoP [EA]](#dpop-ea-1) - [My Account API](#my-account-api) - [Enroll a new passkey](#enroll-a-new-passkey) @@ -1552,11 +1552,7 @@ authentication -## Native to Web SSO login [EA] - -> [!NOTE] -> This feature is currently available in [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). Please reach out to Auth0 support to get it -> enabled for your tenant. +## Native to Web SSO login This feature allows you to authenticate a user in a web session using the refresh token obtained from the native session without requiring the user to log in again. diff --git a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt index eec6bd1fb..312377309 100755 --- a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt @@ -1013,12 +1013,6 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe * parameter with the session transfer token. For example, * `https://example.com/login?session_transfer_token=THE_TOKEN`. * - * ##Availability - * - * This feature is currently available in - * [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). - * Please reach out to Auth0 support to get it enabled for your tenant. - * * * @param refreshToken A valid refresh token obtained as part of Auth0 authentication * @return a request to fetch a session transfer token diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index 31f8e62f4..0cc5c61fa 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt @@ -102,11 +102,6 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting * parameter with the session transfer token. For example, * `https://example.com/login?session_transfer_token=THE_TOKEN`. * - * ## Availability - * - * This feature is currently available in - * [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). - * Please reach out to Auth0 support to get it enabled for your tenant. * * It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid. * This method will handle saving the refresh_token, if a new one is issued. @@ -123,11 +118,6 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting * parameter with the session transfer token. For example, * `https://example.com/login?session_transfer_token=THE_TOKEN`. * - * ## Availability - * - * This feature is currently available in - * [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). - * Please reach out to Auth0 support to get it enabled for your tenant. * * It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid. * This method will handle saving the refresh_token, if a new one is issued. @@ -185,11 +175,6 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting * parameter with the session transfer token. For example, * `https://example.com/login?session_transfer_token=THE_TOKEN`. * - * ## Availability - * - * This feature is currently available in - * [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). - * Please reach out to Auth0 support to get it enabled for your tenant. * * It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid. * This method will handle saving the refresh_token, if a new one is issued. @@ -208,11 +193,6 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting * parameter with the session transfer token. For example, * `https://example.com/login?session_transfer_token=THE_TOKEN`. * - * ## Availability - * - * This feature is currently available in - * [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). - * Please reach out to Auth0 support to get it enabled for your tenant. * * It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid. * This method will handle saving the refresh_token, if a new one is issued. diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index a6e86c492..4a367e778 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -244,12 +244,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * parameter with the session transfer token. For example, * `https://example.com/login?session_transfer_token=THE_TOKEN`. * - * ## Availability - * - * This feature is currently available in - * [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). - * Please reach out to Auth0 support to get it enabled for your tenant. - * * It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid. * This method will handle saving the refresh_token, if a new one is issued. */ @@ -265,11 +259,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * parameter with the session transfer token. For example, * `https://example.com/login?session_transfer_token=THE_TOKEN`. * - * ## Availability - * - * This feature is currently available in - * [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). - * Please reach out to Auth0 support to get it enabled for your tenant. * * It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid. * This method will handle saving the refresh_token, if a new one is issued. @@ -346,12 +335,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * parameter with the session transfer token. For example, * `https://example.com/login?session_transfer_token=THE_TOKEN`. * - * ## Availability - * - * This feature is currently available in - * [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). - * Please reach out to Auth0 support to get it enabled for your tenant. - * * It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid. * This method will handle saving the refresh_token, if a new one is issued. */ @@ -369,12 +352,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * parameter with the session transfer token. For example, * `https://example.com/login?session_transfer_token=THE_TOKEN`. * - * ## Availability - * - * This feature is currently available in - * [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). - * Please reach out to Auth0 support to get it enabled for your tenant. - * * It will fail with [CredentialsManagerException] if the existing refresh_token is null or no longer valid. * This method will handle saving the refresh_token, if a new one is issued. */ From f0ca8f6f963f138ee61c25fdd95fb19ea82bafeb Mon Sep 17 00:00:00 2001 From: Prince Mathew <17837162+pmathew92@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:04:49 +0530 Subject: [PATCH 02/28] chore: Removed claude PR-analyzer workflow (#921) --- .github/workflows/claude-code-review.yml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .github/workflows/claude-code-review.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index d2183bca9..000000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Claude Code PR Review - -on: - issue_comment: - types: [ created ] - pull_request_review_comment: - types: [ created ] - pull_request_review: - types: [ submitted ] - -jobs: - claude-review: - permissions: - contents: write - issues: write - pull-requests: write - id-token: write - uses: auth0/auth0-ai-pr-analyzer-gh-action/.github/workflows/claude-code-review.yml@main \ No newline at end of file From 53a0023474c1c2499c27f5245d617202a25e02c5 Mon Sep 17 00:00:00 2001 From: Prince Mathew <17837162+pmathew92@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:50:08 +0530 Subject: [PATCH 03/28] Gradle agp 8 upgrade (#900) --- .github/actions/maven-publish/action.yml | 2 +- .github/actions/setup/action.yml | 8 +- .github/workflows/codeql.yml | 2 +- .github/workflows/release.yml | 4 +- AGP_UPGRADE_PLAN.md | 399 +++++++ auth0/build.gradle | 32 +- auth0/src/main/AndroidManifest.xml | 3 +- .../java/com/auth0/android/Auth0Test.java | 2 +- .../AuthenticationAPIClientTest.kt | 12 +- .../request/ProfileRequestTest.java | 2 +- .../request/SignUpRequestTest.java | 2 +- .../storage/CredentialsManagerTest.kt | 26 +- .../storage/CryptoUtilTest.java | 1032 ++++++++--------- .../storage/LocalAuthenticationManagerTest.kt | 8 +- ...reCredentialsManagerBiometricPolicyTest.kt | 2 +- .../storage/SecureCredentialsManagerTest.kt | 21 +- .../storage/SharedPreferencesStorageTest.java | 12 +- .../auth0/android/dpop/DPoPKeyStoreTest.kt | 110 +- .../java/com/auth0/android/dpop/DPoPTest.kt | 6 +- .../com/auth0/android/dpop/DPoPUtilTest.kt | 15 +- .../android/management/UsersAPIClientTest.kt | 2 +- .../myaccount/MyAccountAPIClientTest.kt | 2 +- .../provider/AuthenticationActivityTest.kt | 2 +- .../android/provider/BrowserPickerTest.java | 4 +- .../provider/CustomTabsControllerTest.java | 76 +- .../provider/CustomTabsOptionsTest.java | 2 +- .../android/provider/OAuthManagerStateTest.kt | 2 +- .../com/auth0/android/provider/PKCETest.java | 4 +- .../android/provider/PasskeyManagerTest.kt | 18 +- .../provider/PermissionHandlerTest.java | 6 +- .../android/provider/WebAuthProviderTest.kt | 2 +- .../android/request/RetryInterceptorTest.kt | 12 +- .../internal/BaseAuthenticationRequestTest.kt | 2 +- .../request/internal/BaseRequestTest.kt | 14 +- .../CommonThreadSwitcherDelegateTest.kt | 2 +- .../internal/TLS12SocketFactoryTest.java | 8 +- build.gradle | 4 +- gradle.properties | 7 +- gradle/jacoco.gradle | 6 +- gradle/wrapper/gradle-wrapper.properties | 2 +- sample/build.gradle | 13 +- sample/src/main/AndroidManifest.xml | 3 +- 42 files changed, 1078 insertions(+), 815 deletions(-) create mode 100644 AGP_UPGRADE_PLAN.md diff --git a/.github/actions/maven-publish/action.yml b/.github/actions/maven-publish/action.yml index 2aac38757..fb745c124 100644 --- a/.github/actions/maven-publish/action.yml +++ b/.github/actions/maven-publish/action.yml @@ -24,7 +24,7 @@ runs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '11' + java-version: '17' - uses: gradle/wrapper-validation-action@56b90f209b02bf6d1deae490e9ef18b21a389cd4 # pin@1.1.0 diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index d18c43454..c0125fd93 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -5,15 +5,15 @@ inputs: java: description: The Java version to use required: false - default: 8.0.382-tem + default: '17' gradle: description: The Gradle version to use required: false - default: 6.7.1 + default: 8.10.2 kotlin: description: The Kotlin version to use required: false - default: 1.6.21 + default: 2.0.21 runs: using: composite @@ -23,7 +23,7 @@ runs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '11' + java-version: '17' - run: | curl -s "https://get.sdkman.io" | bash diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d9d498320..667562b07 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -39,7 +39,7 @@ jobs: uses: actions/setup-java@v5 with: distribution: 'temurin' - java-version: '11' + java-version: '17' - name: Checkout uses: actions/checkout@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d74a96c02..528fb5400 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: rl-scanner: uses: ./.github/workflows/rl-scanner.yml with: - java-version: 8.0.402-zulu + java-version: '17' artifact-name: 'auth0-release.aar' secrets: RLSECURE_LICENSE: ${{ secrets.RLSECURE_LICENSE }} @@ -32,7 +32,7 @@ jobs: uses: ./.github/workflows/java-release.yml needs: rl-scanner with: - java-version: 8.0.402-zulu + java-version: '17' secrets: ossr-username: ${{ secrets.OSSR_USERNAME }} ossr-token: ${{ secrets.OSSR_TOKEN }} diff --git a/AGP_UPGRADE_PLAN.md b/AGP_UPGRADE_PLAN.md new file mode 100644 index 000000000..a484fb121 --- /dev/null +++ b/AGP_UPGRADE_PLAN.md @@ -0,0 +1,399 @@ +# 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/auth0/build.gradle b/auth0/build.gradle index e3b5cae26..9c0190136 100644 --- a/auth0/build.gradle +++ b/auth0/build.gradle @@ -34,8 +34,13 @@ version = getVersionFromFile() logger.lifecycle("Using version ${version} for ${name}") android { + namespace 'com.auth0.android.auth0' compileSdk 35 + buildFeatures { + buildConfig = true + } + defaultConfig { minSdkVersion 21 targetSdk 35 @@ -50,9 +55,9 @@ android { consumerProguardFiles '../proguard/proguard-gson.pro', '../proguard/proguard-okio.pro', '../proguard/proguard-jetpack.pro' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - lintOptions { - htmlReport true - abortOnError true + lint { + htmlReport = true + abortOnError = true } testOptions { unitTests { @@ -63,11 +68,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() + jvmTarget = JavaVersion.VERSION_17.toString() freeCompilerArgs += [ '-Xexplicit-api=strict', // or '-Xexplicit-api=warning' ] @@ -76,15 +81,14 @@ android { ext { okhttpVersion = '4.12.0' - powermockVersion = '2.0.9' - coroutinesVersion = '1.6.2' + coroutinesVersion = '1.7.3' biometricLibraryVersion = '1.1.0' credentialManagerVersion = "1.3.0" } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.appcompat:appcompat:1.6.0' implementation 'androidx.browser:browser:1.4.0' @@ -99,16 +103,12 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0' - testImplementation "org.powermock:powermock-module-junit4:$powermockVersion" - testImplementation "org.powermock:powermock-module-junit4-rule:$powermockVersion" - testImplementation "org.powermock:powermock-api-mockito2:$powermockVersion" - testImplementation 'org.mockito:mockito-core:3.12.4' - // Mockito-Kotlin: See https://github.com/nhaarman/mockito-kotlin/wiki/Parameter-specified-as-non-null-is-null - testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0' + testImplementation 'org.mockito:mockito-core:5.7.0' + testImplementation 'org.mockito.kotlin:mockito-kotlin:5.1.0' testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" testImplementation "com.squareup.okhttp3:okhttp-tls:$okhttpVersion" testImplementation 'com.jayway.awaitility:awaitility:1.7.0' - testImplementation 'org.robolectric:robolectric:4.8.1' + testImplementation 'org.robolectric:robolectric:4.14.1' testImplementation 'androidx.test.espresso:espresso-intents:3.5.1' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" diff --git a/auth0/src/main/AndroidManifest.xml b/auth0/src/main/AndroidManifest.xml index 96c1233b1..1532d6819 100644 --- a/auth0/src/main/AndroidManifest.xml +++ b/auth0/src/main/AndroidManifest.xml @@ -1,8 +1,7 @@ + xmlns:tools="http://schemas.android.com/tools"> diff --git a/auth0/src/test/java/com/auth0/android/Auth0Test.java b/auth0/src/test/java/com/auth0/android/Auth0Test.java index f924a95bb..52eabfd1b 100755 --- a/auth0/src/test/java/com/auth0/android/Auth0Test.java +++ b/auth0/src/test/java/com/auth0/android/Auth0Test.java @@ -23,7 +23,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import java.lang.reflect.Method; diff --git a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt index 1eb962e33..e83b7cc16 100755 --- a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt @@ -32,12 +32,12 @@ import com.auth0.android.util.SSLTestUtils.testClient import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.HttpUrl.Companion.toHttpUrlOrNull diff --git a/auth0/src/test/java/com/auth0/android/authentication/request/ProfileRequestTest.java b/auth0/src/test/java/com/auth0/android/authentication/request/ProfileRequestTest.java index 6528d46da..3aa931a59 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/request/ProfileRequestTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/request/ProfileRequestTest.java @@ -7,7 +7,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/auth0/src/test/java/com/auth0/android/authentication/request/SignUpRequestTest.java b/auth0/src/test/java/com/auth0/android/authentication/request/SignUpRequestTest.java index 2718fcec6..91caf08c0 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/request/SignUpRequestTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/request/SignUpRequestTest.java @@ -23,7 +23,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt index 4eee35709..c69534fda 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt @@ -16,16 +16,20 @@ import com.auth0.android.result.SSOCredentialsMock import com.auth0.android.result.toAPICredentials import com.auth0.android.util.Clock import com.google.gson.Gson -import com.nhaarman.mockitokotlin2.KArgumentCaptor -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.verifyZeroInteractions +import org.mockito.kotlin.KArgumentCaptor +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.verifyNoMoreInteractions import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest @@ -252,7 +256,7 @@ public class CredentialsManagerTest { @Test public fun shouldNotSaveIfTheSSOCredentialsHasNoRefreshToken() { - verifyZeroInteractions(storage) + verifyNoMoreInteractions(storage) val ssoCredentials = SSOCredentialsMock.create( "accessToken", "identityToken", "issuedTokenType", "tokenType", null, 60 diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java index d9f84c910..1fc567d8d 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java @@ -1,29 +1,43 @@ package com.auth0.android.authentication.storage; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + import android.app.KeyguardManager; import android.content.Context; import android.content.Intent; -import android.os.Build; import android.security.KeyPairGeneratorSpec; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; import android.text.TextUtils; import android.util.Base64; -import android.util.Log; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.stubbing.Answer; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; +import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import org.robolectric.util.ReflectionHelpers; import java.io.IOException; import java.math.BigInteger; @@ -53,37 +67,15 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; -import javax.security.auth.x500.X500Principal; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.greaterThan; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.doReturn; -import static org.powermock.api.mockito.PowerMockito.doThrow; -import static org.powermock.api.mockito.PowerMockito.mock; -import static org.powermock.api.mockito.PowerMockito.verifyPrivate; /** - * In the rest of the test files we use Mockito as that's enough for most cases. However, - * when Kotlin classes are introduced in the project, Mockito fails to mock them because - * they are final by default. - * The solution is to use the 'mockito-inline' plugin. However, when used in combination - * with Powermock, both configuration files clash and the tests fail. - * The MockMaker needs to be set up only in one place, the Powermock configuration file. - *

- * Read more: https://github.com/powermock/powermock/issues/992#issuecomment-662845804 + * This test class uses MockedStatic for static method mocking (KeyStore, Cipher, KeyGenerator, + * KeyPairGenerator, Base64, TextUtils) and relies on Robolectric shadows for Android SDK + * builder classes like KeyGenParameterSpec.Builder and KeyPairGeneratorSpec.Builder. + * Note: Robolectric 4.x requires SDK 21+ (Android 5.0+). */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({CryptoUtil.class, KeyGenerator.class, TextUtils.class, Build.VERSION.class, Base64.class, Cipher.class, Log.class, KeyStore.class}) +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) public class CryptoUtilTest { private static final String RSA_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; @@ -93,15 +85,21 @@ public class CryptoUtilTest { private static final String ANDROID_KEY_STORE = "AndroidKeyStore"; private static final String ALGORITHM_AES = "AES"; private static final String ALGORITHM_RSA = "RSA"; - private static final int RSA_KEY_SIZE = 2048; - private final Storage storage = PowerMockito.mock(Storage.class); - private final Cipher rsaOaepCipher = PowerMockito.mock(Cipher.class); - private final Cipher rsaPkcs1Cipher = PowerMockito.mock(Cipher.class); - private final Cipher aesCipher = PowerMockito.mock(Cipher.class); - private final KeyStore keyStore = PowerMockito.mock(KeyStore.class); - private final KeyPairGenerator keyPairGenerator = PowerMockito.mock(KeyPairGenerator.class); - private final KeyGenerator keyGenerator = PowerMockito.mock(KeyGenerator.class); + private final Storage storage = Mockito.mock(Storage.class); + private final Cipher rsaOaepCipher = Mockito.mock(Cipher.class); + private final Cipher rsaPkcs1Cipher = Mockito.mock(Cipher.class); + private final Cipher aesCipher = Mockito.mock(Cipher.class); + private final KeyStore keyStore = Mockito.mock(KeyStore.class); + private final KeyPairGenerator keyPairGenerator = Mockito.mock(KeyPairGenerator.class); + private final KeyGenerator keyGenerator = Mockito.mock(KeyGenerator.class); + + private MockedStatic keyStoreMock; + private MockedStatic cipherMock; + private MockedStatic keyGeneratorMock; + private MockedStatic keyPairGeneratorMock; + private MockedStatic base64Mock; + private MockedStatic textUtilsMock; private CryptoUtil cryptoUtil; @@ -117,18 +115,49 @@ public class CryptoUtilTest { @Before public void setUp() throws Exception { - PowerMockito.mockStatic(Log.class); - PowerMockito.mockStatic(TextUtils.class); - PowerMockito.when(TextUtils.isEmpty(nullable(String.class))).then((Answer) invocation -> { - String input = invocation.getArgument(0, String.class); - return input == null || input.isEmpty(); + // Initialize MockedStatic instances for static method mocking + keyStoreMock = Mockito.mockStatic(KeyStore.class); + keyStoreMock.when(() -> KeyStore.getInstance(ANDROID_KEY_STORE)).thenReturn(keyStore); + + cipherMock = Mockito.mockStatic(Cipher.class); + cipherMock.when(() -> Cipher.getInstance(anyString())).thenAnswer((Answer) invocation -> { + String transformation = invocation.getArgument(0, String.class); + if (RSA_TRANSFORMATION.equals(transformation)) { + return rsaOaepCipher; + } else if (OLD_RSA_PKCS1_TRANSFORMATION.equals(transformation)) { + return rsaPkcs1Cipher; + } else if (AES_TRANSFORMATION.equals(transformation)) { + return aesCipher; + } + return null; }); + keyGeneratorMock = Mockito.mockStatic(KeyGenerator.class); + keyGeneratorMock.when(() -> KeyGenerator.getInstance(ALGORITHM_AES)).thenReturn(keyGenerator); + + keyPairGeneratorMock = Mockito.mockStatic(KeyPairGenerator.class); + keyPairGeneratorMock.when(() -> KeyPairGenerator.getInstance(ALGORITHM_RSA, ANDROID_KEY_STORE)) + .thenReturn(keyPairGenerator); + + base64Mock = Mockito.mockStatic(Base64.class, Mockito.CALLS_REAL_METHODS); + textUtilsMock = Mockito.mockStatic(TextUtils.class, Mockito.CALLS_REAL_METHODS); + context = mock(Context.class); when(context.getPackageName()).thenReturn(APP_PACKAGE_NAME); cryptoUtil = newCryptoUtilSpy(); } + @After + public void tearDown() { + // Close all MockedStatic instances to prevent memory leaks + if (keyStoreMock != null) keyStoreMock.close(); + if (cipherMock != null) cipherMock.close(); + if (keyGeneratorMock != null) keyGeneratorMock.close(); + if (keyPairGeneratorMock != null) keyPairGeneratorMock.close(); + if (base64Mock != null) base64Mock.close(); + if (textUtilsMock != null) textUtilsMock.close(); + } + /* * GET RSA KEY tests */ @@ -142,49 +171,42 @@ public void shouldThrowWhenRSAKeyAliasIsInvalid() { } @Test - @Config(sdk = 19) - public void shouldNotCreateProtectedRSAKeyPairIfMissingAndLockScreenEnabledOnAPI19() throws Exception { - ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 19); - - PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); - KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); - PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry); - - KeyPairGeneratorSpec spec = PowerMockito.mock(KeyPairGeneratorSpec.class); - KeyPairGeneratorSpec.Builder builder = newKeyPairGeneratorSpecBuilder(spec); - PowerMockito.whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(builder); + @Config(sdk = 21) + public void shouldNotCreateProtectedRSAKeyPairIfMissingAndLockScreenEnabled() throws Exception { + Mockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); + KeyStore.PrivateKeyEntry expectedEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); + Mockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry); - ArgumentCaptor principalCaptor = ArgumentCaptor.forClass(X500Principal.class); - ArgumentCaptor startDateCaptor = ArgumentCaptor.forClass(Date.class); - ArgumentCaptor endDateCaptor = ArgumentCaptor.forClass(Date.class); + ArgumentCaptor specCaptor = ArgumentCaptor.forClass(AlgorithmParameterSpec.class); - //Set LockScreen as Enabled - KeyguardManager kService = PowerMockito.mock(KeyguardManager.class); - PowerMockito.when(context.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService); - PowerMockito.when(kService.isKeyguardSecure()).thenReturn(true); + //Set LockScreen as Enabled but with null device credential intent + KeyguardManager kService = Mockito.mock(KeyguardManager.class); + Mockito.when(context.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService); + Mockito.when(kService.isKeyguardSecure()).thenReturn(true); + Mockito.when(kService.createConfirmDeviceCredentialIntent(nullable(CharSequence.class), nullable(CharSequence.class))).thenReturn(null); final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry(); - Mockito.verify(builder).setKeySize(2048); - Mockito.verify(builder).setSubject(principalCaptor.capture()); - Mockito.verify(builder).setAlias(KEY_ALIAS); - Mockito.verify(builder).setSerialNumber(BigInteger.ONE); - Mockito.verify(builder).setStartDate(startDateCaptor.capture()); - Mockito.verify(builder).setEndDate(endDateCaptor.capture()); - Mockito.verify(builder, never()).setEncryptionRequired(); - Mockito.verify(keyPairGenerator).initialize(spec); + Mockito.verify(keyPairGenerator).initialize(specCaptor.capture()); Mockito.verify(keyPairGenerator).generateKeyPair(); - assertThat(principalCaptor.getValue(), is(notNullValue())); - assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL)); + // Verify the spec properties directly (Robolectric shadows the real builder) + KeyPairGeneratorSpec spec = (KeyPairGeneratorSpec) specCaptor.getValue(); + assertThat(spec.getKeySize(), is(2048)); + assertThat(spec.getKeystoreAlias(), is(KEY_ALIAS)); + assertThat(spec.getSerialNumber(), is(BigInteger.ONE)); + // Note: setEncryptionRequired was NOT called since authIntent is null + + assertThat(spec.getSubjectDN(), is(notNullValue())); + assertThat(spec.getSubjectDN().getName(), is(CERTIFICATE_PRINCIPAL)); - assertThat(startDateCaptor.getValue(), is(notNullValue())); - long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime(); + assertThat(spec.getStartDate(), is(notNullValue())); + long diffMillis = spec.getStartDate().getTime() - new Date().getTime(); long days = TimeUnit.MILLISECONDS.toDays(diffMillis); assertThat(days, is(0L)); //Date is Today - assertThat(endDateCaptor.getValue(), is(notNullValue())); - diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime(); + assertThat(spec.getEndDate(), is(notNullValue())); + diffMillis = spec.getEndDate().getTime() - new Date().getTime(); days = TimeUnit.MILLISECONDS.toDays(diffMillis); assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days @@ -194,48 +216,40 @@ public void shouldNotCreateProtectedRSAKeyPairIfMissingAndLockScreenEnabledOnAPI @Test @Config(sdk = 21) public void shouldCreateUnprotectedRSAKeyPairIfMissingAndLockScreenDisabledOnAPI21() throws Exception { - ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 21); - PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); - KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); - PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry); + Mockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); + KeyStore.PrivateKeyEntry expectedEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); + Mockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry); - KeyPairGeneratorSpec spec = PowerMockito.mock(KeyPairGeneratorSpec.class); - KeyPairGeneratorSpec.Builder builder = newKeyPairGeneratorSpecBuilder(spec); - PowerMockito.whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(builder); - - ArgumentCaptor principalCaptor = ArgumentCaptor.forClass(X500Principal.class); - ArgumentCaptor startDateCaptor = ArgumentCaptor.forClass(Date.class); - ArgumentCaptor endDateCaptor = ArgumentCaptor.forClass(Date.class); + ArgumentCaptor specCaptor = ArgumentCaptor.forClass(AlgorithmParameterSpec.class); //Set LockScreen as Disabled - KeyguardManager kService = PowerMockito.mock(KeyguardManager.class); - PowerMockito.when(context.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService); - PowerMockito.when(kService.isKeyguardSecure()).thenReturn(false); - PowerMockito.when(kService.createConfirmDeviceCredentialIntent(any(CharSequence.class), any(CharSequence.class))).thenReturn(null); + KeyguardManager kService = Mockito.mock(KeyguardManager.class); + Mockito.when(context.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService); + Mockito.when(kService.isKeyguardSecure()).thenReturn(false); + Mockito.when(kService.createConfirmDeviceCredentialIntent(any(CharSequence.class), any(CharSequence.class))).thenReturn(null); final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry(); - Mockito.verify(builder).setKeySize(2048); - Mockito.verify(builder).setSubject(principalCaptor.capture()); - Mockito.verify(builder).setAlias(KEY_ALIAS); - Mockito.verify(builder).setSerialNumber(BigInteger.ONE); - Mockito.verify(builder).setStartDate(startDateCaptor.capture()); - Mockito.verify(builder).setEndDate(endDateCaptor.capture()); - Mockito.verify(builder, never()).setEncryptionRequired(); - Mockito.verify(keyPairGenerator).initialize(spec); + Mockito.verify(keyPairGenerator).initialize(specCaptor.capture()); Mockito.verify(keyPairGenerator).generateKeyPair(); - assertThat(principalCaptor.getValue(), is(notNullValue())); - assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL)); + // Verify the spec properties directly + KeyPairGeneratorSpec spec = (KeyPairGeneratorSpec) specCaptor.getValue(); + assertThat(spec.getKeySize(), is(2048)); + assertThat(spec.getKeystoreAlias(), is(KEY_ALIAS)); + assertThat(spec.getSerialNumber(), is(BigInteger.ONE)); + + assertThat(spec.getSubjectDN(), is(notNullValue())); + assertThat(spec.getSubjectDN().getName(), is(CERTIFICATE_PRINCIPAL)); - assertThat(startDateCaptor.getValue(), is(notNullValue())); - long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime(); + assertThat(spec.getStartDate(), is(notNullValue())); + long diffMillis = spec.getStartDate().getTime() - new Date().getTime(); long days = TimeUnit.MILLISECONDS.toDays(diffMillis); assertThat(days, is(0L)); //Date is Today - assertThat(endDateCaptor.getValue(), is(notNullValue())); - diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime(); + assertThat(spec.getEndDate(), is(notNullValue())); + diffMillis = spec.getEndDate().getTime() - new Date().getTime(); days = TimeUnit.MILLISECONDS.toDays(diffMillis); assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days @@ -245,48 +259,42 @@ public void shouldCreateUnprotectedRSAKeyPairIfMissingAndLockScreenDisabledOnAPI @Test @Config(sdk = 21) public void shouldCreateProtectedRSAKeyPairIfMissingAndLockScreenEnabledOnAPI21() throws Exception { - ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 21); - PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); - KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); - PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry); + Mockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); + KeyStore.PrivateKeyEntry expectedEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); + Mockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry); - KeyPairGeneratorSpec spec = PowerMockito.mock(KeyPairGeneratorSpec.class); - KeyPairGeneratorSpec.Builder builder = newKeyPairGeneratorSpecBuilder(spec); - PowerMockito.whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(builder); - - ArgumentCaptor principalCaptor = ArgumentCaptor.forClass(X500Principal.class); - ArgumentCaptor startDateCaptor = ArgumentCaptor.forClass(Date.class); - ArgumentCaptor endDateCaptor = ArgumentCaptor.forClass(Date.class); + ArgumentCaptor specCaptor = ArgumentCaptor.forClass(AlgorithmParameterSpec.class); //Set LockScreen as Enabled - KeyguardManager kService = PowerMockito.mock(KeyguardManager.class); - PowerMockito.when(context.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService); - PowerMockito.when(kService.isKeyguardSecure()).thenReturn(true); - PowerMockito.when(kService.createConfirmDeviceCredentialIntent(any(), any())).thenReturn(new Intent()); + KeyguardManager kService = Mockito.mock(KeyguardManager.class); + Mockito.when(context.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService); + Mockito.when(kService.isKeyguardSecure()).thenReturn(true); + Mockito.when(kService.createConfirmDeviceCredentialIntent(any(), any())).thenReturn(new Intent()); final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry(); - Mockito.verify(builder).setKeySize(2048); - Mockito.verify(builder).setSubject(principalCaptor.capture()); - Mockito.verify(builder).setAlias(KEY_ALIAS); - Mockito.verify(builder).setSerialNumber(BigInteger.ONE); - Mockito.verify(builder).setStartDate(startDateCaptor.capture()); - Mockito.verify(builder).setEndDate(endDateCaptor.capture()); - Mockito.verify(builder).setEncryptionRequired(); - Mockito.verify(keyPairGenerator).initialize(spec); + Mockito.verify(keyPairGenerator).initialize(specCaptor.capture()); Mockito.verify(keyPairGenerator).generateKeyPair(); - assertThat(principalCaptor.getValue(), is(notNullValue())); - assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL)); + // Verify the spec properties directly + KeyPairGeneratorSpec spec = (KeyPairGeneratorSpec) specCaptor.getValue(); + assertThat(spec.getKeySize(), is(2048)); + assertThat(spec.getKeystoreAlias(), is(KEY_ALIAS)); + assertThat(spec.getSerialNumber(), is(BigInteger.ONE)); + // Note: setEncryptionRequired WAS called since lock screen is enabled with valid authIntent + assertThat(spec.isEncryptionRequired(), is(true)); + + assertThat(spec.getSubjectDN(), is(notNullValue())); + assertThat(spec.getSubjectDN().getName(), is(CERTIFICATE_PRINCIPAL)); - assertThat(startDateCaptor.getValue(), is(notNullValue())); - long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime(); + assertThat(spec.getStartDate(), is(notNullValue())); + long diffMillis = spec.getStartDate().getTime() - new Date().getTime(); long days = TimeUnit.MILLISECONDS.toDays(diffMillis); assertThat(days, is(0L)); //Date is Today - assertThat(endDateCaptor.getValue(), is(notNullValue())); - diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime(); + assertThat(spec.getEndDate(), is(notNullValue())); + diffMillis = spec.getEndDate().getTime() - new Date().getTime(); days = TimeUnit.MILLISECONDS.toDays(diffMillis); assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days @@ -296,45 +304,37 @@ public void shouldCreateProtectedRSAKeyPairIfMissingAndLockScreenEnabledOnAPI21( @Test @Config(sdk = 23) public void shouldCreateRSAKeyPairIfMissingOnAPI23AndUp() throws Exception { - ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 23); - - PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); - KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); - PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry); - - KeyGenParameterSpec spec = PowerMockito.mock(KeyGenParameterSpec.class); - KeyGenParameterSpec.Builder builder = newKeyGenParameterSpecBuilder(spec); - PowerMockito.whenNew(KeyGenParameterSpec.Builder.class).withArguments(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT).thenReturn(builder); - ArgumentCaptor principalCaptor = ArgumentCaptor.forClass(X500Principal.class); - ArgumentCaptor startDateCaptor = ArgumentCaptor.forClass(Date.class); - ArgumentCaptor endDateCaptor = ArgumentCaptor.forClass(Date.class); + Mockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); + KeyStore.PrivateKeyEntry expectedEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); + Mockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry); + ArgumentCaptor specCaptor = ArgumentCaptor.forClass(AlgorithmParameterSpec.class); final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry(); - - Mockito.verify(builder).setKeySize(2048); - Mockito.verify(builder).setCertificateSubject(principalCaptor.capture()); - Mockito.verify(builder).setCertificateSerialNumber(BigInteger.ONE); - Mockito.verify(builder).setCertificateNotBefore(startDateCaptor.capture()); - Mockito.verify(builder).setCertificateNotAfter(endDateCaptor.capture()); - Mockito.verify(builder).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP); - Mockito.verify(builder).setDigests(KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA256); - Mockito.verify(builder).setBlockModes(KeyProperties.BLOCK_MODE_ECB); - Mockito.verify(keyPairGenerator).initialize(spec); + Mockito.verify(keyPairGenerator).initialize(specCaptor.capture()); Mockito.verify(keyPairGenerator).generateKeyPair(); - assertThat(principalCaptor.getValue(), is(notNullValue())); - assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL)); + // Verify the spec properties directly + KeyGenParameterSpec spec = (KeyGenParameterSpec) specCaptor.getValue(); + assertThat(spec.getKeySize(), is(2048)); + assertThat(spec.getKeystoreAlias(), is(KEY_ALIAS)); + assertThat(spec.getCertificateSerialNumber(), is(BigInteger.ONE)); + assertThat(spec.getEncryptionPaddings(), is(new String[]{KeyProperties.ENCRYPTION_PADDING_RSA_OAEP})); + assertThat(spec.getDigests(), is(new String[]{KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA256})); + assertThat(spec.getBlockModes(), is(new String[]{KeyProperties.BLOCK_MODE_ECB})); + + assertThat(spec.getCertificateSubject(), is(notNullValue())); + assertThat(spec.getCertificateSubject().getName(), is(CERTIFICATE_PRINCIPAL)); - assertThat(startDateCaptor.getValue(), is(notNullValue())); - long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime(); + assertThat(spec.getCertificateNotBefore(), is(notNullValue())); + long diffMillis = spec.getCertificateNotBefore().getTime() - new Date().getTime(); long days = TimeUnit.MILLISECONDS.toDays(diffMillis); assertThat(days, is(0L)); //Date is Today - assertThat(endDateCaptor.getValue(), is(notNullValue())); - diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime(); + assertThat(spec.getCertificateNotAfter(), is(notNullValue())); + diffMillis = spec.getCertificateNotAfter().getTime() - new Date().getTime(); days = TimeUnit.MILLISECONDS.toDays(diffMillis); assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days @@ -344,44 +344,37 @@ public void shouldCreateRSAKeyPairIfMissingOnAPI23AndUp() throws Exception { @Test @Config(sdk = 28) public void shouldCreateRSAKeyPairIfMissingOnAPI28AndUp() throws Exception { - ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 28); - - PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); - KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); - PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry); - KeyGenParameterSpec spec = PowerMockito.mock(KeyGenParameterSpec.class); - KeyGenParameterSpec.Builder builder = newKeyGenParameterSpecBuilder(spec); - PowerMockito.whenNew(KeyGenParameterSpec.Builder.class).withArguments(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT).thenReturn(builder); - - ArgumentCaptor principalCaptor = ArgumentCaptor.forClass(X500Principal.class); - ArgumentCaptor startDateCaptor = ArgumentCaptor.forClass(Date.class); - ArgumentCaptor endDateCaptor = ArgumentCaptor.forClass(Date.class); + Mockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); + KeyStore.PrivateKeyEntry expectedEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); + Mockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry); + ArgumentCaptor specCaptor = ArgumentCaptor.forClass(AlgorithmParameterSpec.class); final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry(); - Mockito.verify(builder).setKeySize(2048); - Mockito.verify(builder).setCertificateSubject(principalCaptor.capture()); - Mockito.verify(builder).setCertificateSerialNumber(BigInteger.ONE); - Mockito.verify(builder).setCertificateNotBefore(startDateCaptor.capture()); - Mockito.verify(builder).setCertificateNotAfter(endDateCaptor.capture()); - Mockito.verify(builder).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP); - Mockito.verify(builder).setDigests(KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA256); - Mockito.verify(builder).setBlockModes(KeyProperties.BLOCK_MODE_ECB); - Mockito.verify(keyPairGenerator).initialize(spec); + Mockito.verify(keyPairGenerator).initialize(specCaptor.capture()); Mockito.verify(keyPairGenerator).generateKeyPair(); - assertThat(principalCaptor.getValue(), is(notNullValue())); - assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL)); + // Verify the spec properties directly + KeyGenParameterSpec spec = (KeyGenParameterSpec) specCaptor.getValue(); + assertThat(spec.getKeySize(), is(2048)); + assertThat(spec.getKeystoreAlias(), is(KEY_ALIAS)); + assertThat(spec.getCertificateSerialNumber(), is(BigInteger.ONE)); + assertThat(spec.getEncryptionPaddings(), is(new String[]{KeyProperties.ENCRYPTION_PADDING_RSA_OAEP})); + assertThat(spec.getDigests(), is(new String[]{KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA256})); + assertThat(spec.getBlockModes(), is(new String[]{KeyProperties.BLOCK_MODE_ECB})); + + assertThat(spec.getCertificateSubject(), is(notNullValue())); + assertThat(spec.getCertificateSubject().getName(), is(CERTIFICATE_PRINCIPAL)); - assertThat(startDateCaptor.getValue(), is(notNullValue())); - long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime(); + assertThat(spec.getCertificateNotBefore(), is(notNullValue())); + long diffMillis = spec.getCertificateNotBefore().getTime() - new Date().getTime(); long days = TimeUnit.MILLISECONDS.toDays(diffMillis); assertThat(days, is(0L)); //Date is Today - assertThat(endDateCaptor.getValue(), is(notNullValue())); - diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime(); + assertThat(spec.getCertificateNotAfter(), is(notNullValue())); + diffMillis = spec.getCertificateNotAfter().getTime() - new Date().getTime(); days = TimeUnit.MILLISECONDS.toDays(diffMillis); assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days @@ -391,55 +384,42 @@ public void shouldCreateRSAKeyPairIfMissingOnAPI28AndUp() throws Exception { @Test @Config(sdk = 28) public void shouldCreateNewRSAKeyPairWhenExistingRSAKeyPairCannotBeRebuiltOnAPI28AndUp() throws Exception { - ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 28); - PrivateKey privateKey = PowerMockito.mock(PrivateKey.class); + PrivateKey privateKey = Mockito.mock(PrivateKey.class); - //This is required to trigger the fallback when alias is present but key is not - PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); - PowerMockito.when(keyStore.getKey(KEY_ALIAS, null)).thenReturn(privateKey).thenReturn(null); - PowerMockito.when(keyStore.getCertificate(KEY_ALIAS)).thenReturn(null); - //This is required to trigger finding the key after generating it - KeyStore.PrivateKeyEntry expectedEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); - PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry); - - //Tests no instantiation of PrivateKeyEntry - PowerMockito.verifyZeroInteractions(KeyStore.PrivateKeyEntry.class); - - //Creation assertion - KeyGenParameterSpec spec = PowerMockito.mock(KeyGenParameterSpec.class); - KeyGenParameterSpec.Builder builder = newKeyGenParameterSpecBuilder(spec); - PowerMockito.whenNew(KeyGenParameterSpec.Builder.class).withArguments(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT).thenReturn(builder); - - ArgumentCaptor principalCaptor = ArgumentCaptor.forClass(X500Principal.class); - ArgumentCaptor startDateCaptor = ArgumentCaptor.forClass(Date.class); - ArgumentCaptor endDateCaptor = ArgumentCaptor.forClass(Date.class); + Mockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); + Mockito.when(keyStore.getKey(KEY_ALIAS, null)).thenReturn(privateKey).thenReturn(null); + Mockito.when(keyStore.getCertificate(KEY_ALIAS)).thenReturn(null); + KeyStore.PrivateKeyEntry expectedEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); + Mockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(expectedEntry); + ArgumentCaptor specCaptor = ArgumentCaptor.forClass(AlgorithmParameterSpec.class); final KeyStore.PrivateKeyEntry entry = cryptoUtil.getRSAKeyEntry(); - Mockito.verify(builder).setKeySize(2048); - Mockito.verify(builder).setCertificateSubject(principalCaptor.capture()); - Mockito.verify(builder).setCertificateSerialNumber(BigInteger.ONE); - Mockito.verify(builder).setCertificateNotBefore(startDateCaptor.capture()); - Mockito.verify(builder).setCertificateNotAfter(endDateCaptor.capture()); - Mockito.verify(builder).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP); - Mockito.verify(builder).setDigests(KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA256); - Mockito.verify(builder).setBlockModes(KeyProperties.BLOCK_MODE_ECB); - Mockito.verify(keyPairGenerator).initialize(spec); + Mockito.verify(keyPairGenerator).initialize(specCaptor.capture()); Mockito.verify(keyPairGenerator).generateKeyPair(); - assertThat(principalCaptor.getValue(), is(notNullValue())); - assertThat(principalCaptor.getValue().getName(), is(CERTIFICATE_PRINCIPAL)); + // Verify the spec properties directly + KeyGenParameterSpec spec = (KeyGenParameterSpec) specCaptor.getValue(); + assertThat(spec.getKeySize(), is(2048)); + assertThat(spec.getKeystoreAlias(), is(KEY_ALIAS)); + assertThat(spec.getCertificateSerialNumber(), is(BigInteger.ONE)); + assertThat(spec.getEncryptionPaddings(), is(new String[]{KeyProperties.ENCRYPTION_PADDING_RSA_OAEP})); + assertThat(spec.getDigests(), is(new String[]{KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA256})); + assertThat(spec.getBlockModes(), is(new String[]{KeyProperties.BLOCK_MODE_ECB})); + + assertThat(spec.getCertificateSubject(), is(notNullValue())); + assertThat(spec.getCertificateSubject().getName(), is(CERTIFICATE_PRINCIPAL)); - assertThat(startDateCaptor.getValue(), is(notNullValue())); - long diffMillis = startDateCaptor.getValue().getTime() - new Date().getTime(); + assertThat(spec.getCertificateNotBefore(), is(notNullValue())); + long diffMillis = spec.getCertificateNotBefore().getTime() - new Date().getTime(); long days = TimeUnit.MILLISECONDS.toDays(diffMillis); assertThat(days, is(0L)); //Date is Today - assertThat(endDateCaptor.getValue(), is(notNullValue())); - diffMillis = endDateCaptor.getValue().getTime() - new Date().getTime(); + assertThat(spec.getCertificateNotAfter(), is(notNullValue())); + diffMillis = spec.getCertificateNotAfter().getTime() - new Date().getTime(); days = TimeUnit.MILLISECONDS.toDays(diffMillis); - assertThat(days, is(greaterThan(25 * 365L))); //Date more than 25 Years in days + assertThat(days, is(greaterThan(25 * 365L))); assertThat(entry, is(expectedEntry)); } @@ -447,68 +427,57 @@ public void shouldCreateNewRSAKeyPairWhenExistingRSAKeyPairCannotBeRebuiltOnAPI2 @Test @Config(sdk = 28) public void shouldUseExistingRSAKeyPairRebuildingTheEntryOnAPI28AndUp() throws Exception { - ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 28); - KeyStore.PrivateKeyEntry entry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); - PrivateKey privateKey = PowerMockito.mock(PrivateKey.class); - Certificate certificate = PowerMockito.mock(Certificate.class); + PrivateKey privateKey = Mockito.mock(PrivateKey.class); + Certificate certificate = Mockito.mock(Certificate.class); - ArgumentCaptor varargsCaptor = ArgumentCaptor.forClass(Object.class); - PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); - PowerMockito.when(keyStore.getKey(KEY_ALIAS, null)).thenReturn(privateKey); - PowerMockito.when(keyStore.getCertificate(KEY_ALIAS)).thenReturn(certificate); - PowerMockito.whenNew(KeyStore.PrivateKeyEntry.class).withAnyArguments().thenReturn(entry); + Mockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); + Mockito.when(keyStore.getKey(KEY_ALIAS, null)).thenReturn(privateKey); + Mockito.when(keyStore.getCertificate(KEY_ALIAS)).thenReturn(certificate); - KeyStore.PrivateKeyEntry rsaEntry = cryptoUtil.getRSAKeyEntry(); - PowerMockito.verifyNew(KeyStore.PrivateKeyEntry.class).withArguments(varargsCaptor.capture()); - assertThat(rsaEntry, is(notNullValue())); - assertThat(rsaEntry, is(entry)); - assertThat(varargsCaptor.getAllValues(), is(notNullValue())); - PrivateKey capturedPrivateKey = (PrivateKey) varargsCaptor.getAllValues().get(0); - Certificate[] capturedCertificatesArray = (Certificate[]) varargsCaptor.getAllValues().get(1); - assertThat(capturedPrivateKey, is(privateKey)); - assertThat(capturedCertificatesArray[0], is(certificate)); - assertThat(capturedCertificatesArray.length, is(1)); + // Use mockConstruction to intercept PrivateKeyEntry constructor + try (MockedConstruction mockedConstruction = Mockito.mockConstruction( + KeyStore.PrivateKeyEntry.class, + (mock, context) -> { + // Capture constructor arguments + })) { + + KeyStore.PrivateKeyEntry rsaEntry = cryptoUtil.getRSAKeyEntry(); + + assertThat(rsaEntry, is(notNullValue())); + assertThat(mockedConstruction.constructed().size(), is(1)); + assertThat(rsaEntry, is(mockedConstruction.constructed().get(0))); + } } @Test @Config(sdk = 28) public void shouldUseExistingPrivateKeyForOldKeyAlias() throws Exception { - ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 28); - KeyStore.PrivateKeyEntry entry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); - PrivateKey privateKey = PowerMockito.mock(PrivateKey.class); - Certificate certificate = PowerMockito.mock(Certificate.class); + PrivateKey privateKey = Mockito.mock(PrivateKey.class); + Certificate certificate = Mockito.mock(Certificate.class); - KeyGenParameterSpec.Builder builder = PowerMockito.mock(KeyGenParameterSpec.Builder.class); - PowerMockito.when(builder.setKeySize(anyInt())).thenReturn(builder); - PowerMockito.when(builder.setCertificateSubject(any(X500Principal.class))).thenReturn(builder); + Mockito.when(keyStore.containsAlias(OLD_KEY_ALIAS)).thenReturn(true); + Mockito.when(keyStore.getKey(OLD_KEY_ALIAS, null)).thenReturn(privateKey); + Mockito.when(keyStore.getCertificate(OLD_KEY_ALIAS)).thenReturn(certificate); - ArgumentCaptor varargsCaptor = ArgumentCaptor.forClass(Object.class); - PowerMockito.when(keyStore.containsAlias(OLD_KEY_ALIAS)).thenReturn(true); - PowerMockito.when(keyStore.getKey(OLD_KEY_ALIAS, null)).thenReturn(privateKey); - PowerMockito.when(keyStore.getCertificate(OLD_KEY_ALIAS)).thenReturn(certificate); - PowerMockito.whenNew(KeyStore.PrivateKeyEntry.class).withAnyArguments().thenReturn(entry); + try (MockedConstruction mockedConstruction = Mockito.mockConstruction( + KeyStore.PrivateKeyEntry.class, + (mock, context) -> { + })) { - KeyStore.PrivateKeyEntry rsaEntry = cryptoUtil.getRSAKeyEntry(); - PowerMockito.verifyNew(KeyStore.PrivateKeyEntry.class).withArguments(varargsCaptor.capture()); - assertThat(rsaEntry, is(notNullValue())); - assertThat(rsaEntry, is(entry)); - assertThat(varargsCaptor.getAllValues(), is(notNullValue())); - PrivateKey capturedPrivateKey = (PrivateKey) varargsCaptor.getAllValues().get(0); - Certificate[] capturedCertificatesArray = (Certificate[]) varargsCaptor.getAllValues().get(1); - assertThat(capturedPrivateKey, is(privateKey)); - assertThat(capturedCertificatesArray[0], is(certificate)); - assertThat(capturedCertificatesArray.length, is(1)); + KeyStore.PrivateKeyEntry rsaEntry = cryptoUtil.getRSAKeyEntry(); + + assertThat(rsaEntry, is(notNullValue())); + assertThat(mockedConstruction.constructed().size(), is(1)); + assertThat(rsaEntry, is(mockedConstruction.constructed().get(0))); + } } @Test @Config(sdk = 28) public void shouldUseExistingRSAKeyPairOnAPI28AndUp() throws Exception { - ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 28); - KeyStore.PrivateKeyEntry entry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); - PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(entry); - PrivateKey privateKey = null; - PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); - PowerMockito.when(keyStore.getKey(KEY_ALIAS, null)).thenReturn(privateKey); + KeyStore.PrivateKeyEntry entry = Mockito.mock(KeyStore.PrivateKeyEntry.class); + Mockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(entry); + Mockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); KeyStore.PrivateKeyEntry rsaEntry = cryptoUtil.getRSAKeyEntry(); assertThat(rsaEntry, is(notNullValue())); @@ -518,10 +487,9 @@ public void shouldUseExistingRSAKeyPairOnAPI28AndUp() throws Exception { @Test @Config(sdk = 27) public void shouldUseExistingRSAKeyPairOnAPI27AndDown() throws Exception { - ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 27); - KeyStore.PrivateKeyEntry entry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); - PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); - PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(entry); + KeyStore.PrivateKeyEntry entry = Mockito.mock(KeyStore.PrivateKeyEntry.class); + Mockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); + Mockito.when(keyStore.getEntry(KEY_ALIAS, null)).thenReturn(entry); KeyStore.PrivateKeyEntry rsaEntry = cryptoUtil.getRSAKeyEntry(); assertThat(rsaEntry, is(notNullValue())); @@ -532,9 +500,9 @@ public void shouldUseExistingRSAKeyPairOnAPI27AndDown() throws Exception { public void shouldDeleteRSAAndAESKeysAndThrowOnUnrecoverableEntryExceptionWhenTryingToObtainRSAKeys() throws Exception { Assert.assertThrows("The existing RSA key pair could not be recovered and has been deleted. " + "This occasionally happens when the Lock Screen settings are changed. You can safely retry this operation.", CryptoException.class, () -> { - KeyStore.PrivateKeyEntry entry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); - PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); - PowerMockito.when(keyStore.getEntry(KEY_ALIAS, null)) + KeyStore.PrivateKeyEntry entry = Mockito.mock(KeyStore.PrivateKeyEntry.class); + Mockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); + Mockito.when(keyStore.getEntry(KEY_ALIAS, null)) .thenThrow(new UnrecoverableEntryException()) .thenReturn(entry); @@ -591,8 +559,7 @@ public void shouldDeleteAESKeysAndThrowOnDoubleIOExceptionWhenTryingToObtainRSAK @Test public void shouldThrowOnKeyStoreExceptionWhenTryingToObtainRSAKeys() { Assert.assertThrows("The device is not compatible with the CryptoUtil class", IncompatibleDeviceException.class, () -> { - PowerMockito.mockStatic(KeyStore.class); - PowerMockito.when(KeyStore.getInstance(anyString())) + Mockito.when(KeyStore.getInstance(anyString())) .thenThrow(new KeyStoreException()); cryptoUtil.getRSAKeyEntry(); @@ -619,15 +586,10 @@ public void shouldThrowOnProviderExceptionWhenTryingToObtainRSAKeys() { @Test public void shouldThrowOnNoSuchProviderExceptionWhenTryingToObtainRSAKeys() { - ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 19); Assert.assertThrows("The device is not compatible with the CryptoUtil class", IncompatibleDeviceException.class, () -> { - PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); - KeyPairGeneratorSpec spec = PowerMockito.mock(KeyPairGeneratorSpec.class); - KeyPairGeneratorSpec.Builder builder = newKeyPairGeneratorSpecBuilder(spec); - PowerMockito.whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(builder); + Mockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); - PowerMockito.mockStatic(KeyPairGenerator.class); - PowerMockito.when(KeyPairGenerator.getInstance(ALGORITHM_RSA, ANDROID_KEY_STORE)) + keyPairGeneratorMock.when(() -> KeyPairGenerator.getInstance(ALGORITHM_RSA, ANDROID_KEY_STORE)) .thenThrow(new NoSuchProviderException()); cryptoUtil.getRSAKeyEntry(); @@ -636,15 +598,10 @@ public void shouldThrowOnNoSuchProviderExceptionWhenTryingToObtainRSAKeys() { @Test public void shouldThrowOnNoSuchAlgorithmExceptionWhenTryingToObtainRSAKeys() { - ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 19); Assert.assertThrows("The device is not compatible with the CryptoUtil class", IncompatibleDeviceException.class, () -> { - PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); - KeyPairGeneratorSpec spec = PowerMockito.mock(KeyPairGeneratorSpec.class); - KeyPairGeneratorSpec.Builder builder = newKeyPairGeneratorSpecBuilder(spec); - PowerMockito.whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(builder); + Mockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); - PowerMockito.mockStatic(KeyPairGenerator.class); - PowerMockito.when(KeyPairGenerator.getInstance(ALGORITHM_RSA, ANDROID_KEY_STORE)) + keyPairGeneratorMock.when(() -> KeyPairGenerator.getInstance(ALGORITHM_RSA, ANDROID_KEY_STORE)) .thenThrow(new NoSuchAlgorithmException()); cryptoUtil.getRSAKeyEntry(); @@ -653,12 +610,8 @@ public void shouldThrowOnNoSuchAlgorithmExceptionWhenTryingToObtainRSAKeys() { @Test public void shouldThrowOnInvalidAlgorithmParameterExceptionWhenTryingToObtainRSAKeys() { - ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 19); Assert.assertThrows("The device is not compatible with the CryptoUtil class", IncompatibleDeviceException.class, () -> { - PowerMockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); - KeyPairGeneratorSpec spec = PowerMockito.mock(KeyPairGeneratorSpec.class); - KeyPairGeneratorSpec.Builder builder = newKeyPairGeneratorSpecBuilder(spec); - PowerMockito.whenNew(KeyPairGeneratorSpec.Builder.class).withAnyArguments().thenReturn(builder); + Mockito.when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(false); doThrow(new InvalidAlgorithmParameterException()).when(keyPairGenerator).initialize(any(AlgorithmParameterSpec.class)); @@ -673,17 +626,15 @@ public void shouldThrowOnInvalidAlgorithmParameterExceptionWhenTryingToObtainRSA @Test public void shouldCreateAESKeyIfMissing() throws Exception { byte[] sampleBytes = new byte[]{0, 1, 2, 3, 4, 5}; - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.encode(sampleBytes, Base64.DEFAULT)).thenReturn("data".getBytes()); - PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn(null); - PowerMockito.when(storage.retrieveString(OLD_KEY_ALIAS)).thenReturn(null); - PowerMockito.mockStatic(TextUtils.class); - PowerMockito.when(TextUtils.isEmpty(null)).thenReturn(true); - - SecretKey secretKey = PowerMockito.mock(SecretKey.class); - PowerMockito.when(keyGenerator.generateKey()).thenReturn(secretKey); - PowerMockito.when(secretKey.getEncoded()).thenReturn(sampleBytes); - PowerMockito.doReturn(sampleBytes).when(cryptoUtil, "RSAEncrypt", sampleBytes); + base64Mock.when(() -> Base64.encode(sampleBytes, Base64.DEFAULT)).thenReturn("data".getBytes()); + Mockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn(null); + Mockito.when(storage.retrieveString(OLD_KEY_ALIAS)).thenReturn(null); + textUtilsMock.when(() -> TextUtils.isEmpty(null)).thenReturn(true); + + SecretKey secretKey = Mockito.mock(SecretKey.class); + Mockito.when(keyGenerator.generateKey()).thenReturn(secretKey); + Mockito.when(secretKey.getEncoded()).thenReturn(sampleBytes); + Mockito.doReturn(sampleBytes).when(cryptoUtil).RSAEncrypt(sampleBytes); final byte[] aesKey = cryptoUtil.getAESKey(); @@ -700,22 +651,21 @@ public void shouldCreateAESKeyIfStoredOneIsEmpty() throws BadPaddingException, I String emptyString = ""; byte[] sampleBytes = emptyString.getBytes(); byte[] sampleOutput = new byte[]{99, 33, 11}; - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode(emptyString, Base64.DEFAULT)).thenReturn(sampleBytes); - PowerMockito.when(Base64.encode(sampleBytes, Base64.DEFAULT)).thenReturn("data".getBytes()); - PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn(emptyString); + base64Mock.when(() -> Base64.decode(emptyString, Base64.DEFAULT)).thenReturn(sampleBytes); + base64Mock.when(() -> Base64.encode(sampleBytes, Base64.DEFAULT)).thenReturn("data".getBytes()); + Mockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn(emptyString); doReturn(sampleBytes).when(cryptoUtil).RSAEncrypt(sampleBytes); //Assume RSAKeyEntry exists - PrivateKey privateKey = PowerMockito.mock(PrivateKey.class); - KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); + PrivateKey privateKey = Mockito.mock(PrivateKey.class); + KeyStore.PrivateKeyEntry privateKeyEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); doReturn(privateKey).when(privateKeyEntry).getPrivateKey(); doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry(); doReturn(sampleOutput).when(rsaOaepCipher).doFinal(sampleBytes); - SecretKey secretKey = PowerMockito.mock(SecretKey.class); - PowerMockito.when(secretKey.getEncoded()).thenReturn(sampleBytes); - PowerMockito.when(keyGenerator.generateKey()).thenReturn(secretKey); + SecretKey secretKey = Mockito.mock(SecretKey.class); + Mockito.when(secretKey.getEncoded()).thenReturn(sampleBytes); + Mockito.when(keyGenerator.generateKey()).thenReturn(secretKey); final byte[] aesKey = cryptoUtil.getAESKey(); @@ -735,9 +685,8 @@ public void shouldUseExistingAESKey() { Arrays.fill(sampleBytes, (byte) 1); String aesString = "non null string"; - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode(aesString, Base64.DEFAULT)).thenReturn(sampleBytes); - PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn(aesString); + base64Mock.when(() -> Base64.decode(aesString, Base64.DEFAULT)).thenReturn(sampleBytes); + Mockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn(aesString); doReturn(sampleBytes).when(cryptoUtil).RSADecrypt(sampleBytes); final byte[] aesKey = cryptoUtil.getAESKey(); @@ -748,12 +697,10 @@ public void shouldUseExistingAESKey() { @Test public void shouldThrowOnNoSuchAlgorithmExceptionWhenCreatingAESKey() throws Exception { Assert.assertThrows("The device is not compatible with the CryptoUtil class", IncompatibleDeviceException.class, () -> { - PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn(null); - PowerMockito.when(storage.retrieveString(OLD_KEY_ALIAS)).thenReturn(null); - PowerMockito.mockStatic(TextUtils.class); - PowerMockito.when(TextUtils.isEmpty(null)).thenReturn(true); - PowerMockito.mockStatic(KeyGenerator.class); - PowerMockito.when(KeyGenerator.getInstance(ALGORITHM_AES)) + Mockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn(null); + Mockito.when(storage.retrieveString(OLD_KEY_ALIAS)).thenReturn(null); + textUtilsMock.when(() -> TextUtils.isEmpty(null)).thenReturn(true); + Mockito.when(KeyGenerator.getInstance(ALGORITHM_AES)) .thenThrow(new NoSuchAlgorithmException()); cryptoUtil.getAESKey(); @@ -769,10 +716,10 @@ public void shouldRSAEncryptData() throws Exception { byte[] sampleInput = new byte[]{0, 1, 2, 3, 4, 5}; byte[] sampleOutput = new byte[]{99, 33, 11}; - PublicKey publicKey = PowerMockito.mock(PublicKey.class); - Certificate certificate = PowerMockito.mock(Certificate.class); + PublicKey publicKey = Mockito.mock(PublicKey.class); + Certificate certificate = Mockito.mock(Certificate.class); doReturn(publicKey).when(certificate).getPublicKey(); - KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); + KeyStore.PrivateKeyEntry privateKeyEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); doReturn(certificate).when(privateKeyEntry).getCertificate(); doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry(); doReturn(sampleOutput).when(rsaOaepCipher).doFinal(sampleInput); @@ -787,14 +734,13 @@ public void shouldRSAEncryptData() throws Exception { public void shouldThrowOnInvalidKeyExceptionWhenTryingToRSAEncrypt() { Assert.assertThrows("The device is not compatible with the CryptoUtil class", IncompatibleDeviceException.class, () -> { byte[] sampleBytes = new byte[0]; - PublicKey publicKey = PowerMockito.mock(PublicKey.class); - Certificate certificate = PowerMockito.mock(Certificate.class); + PublicKey publicKey = Mockito.mock(PublicKey.class); + Certificate certificate = Mockito.mock(Certificate.class); doReturn(publicKey).when(certificate).getPublicKey(); - KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); + KeyStore.PrivateKeyEntry privateKeyEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); doReturn(certificate).when(privateKeyEntry).getCertificate(); doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenReturn(rsaOaepCipher); + Mockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenReturn(rsaOaepCipher); doThrow(new InvalidKeyException()).when(rsaOaepCipher).init(eq(Cipher.ENCRYPT_MODE), eq(publicKey), any(AlgorithmParameterSpec.class)); cryptoUtil.RSAEncrypt(sampleBytes); @@ -806,13 +752,12 @@ public void shouldDeleteAESKeysAndThrowOnBadPaddingExceptionWhenTryingToRSAEncry Assert.assertThrows("The RSA decrypted input is invalid.", CryptoException.class, () -> { byte[] sampleBytes = new byte[0]; - Certificate certificate = PowerMockito.mock(Certificate.class); - KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); + Certificate certificate = Mockito.mock(Certificate.class); + KeyStore.PrivateKeyEntry privateKeyEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); doReturn(certificate).when(privateKeyEntry).getCertificate(); doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenReturn(rsaOaepCipher); - PowerMockito.when(rsaOaepCipher.doFinal(sampleBytes)).thenThrow(new BadPaddingException()); + Mockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenReturn(rsaOaepCipher); + Mockito.when(rsaOaepCipher.doFinal(sampleBytes)).thenThrow(new BadPaddingException()); cryptoUtil.RSAEncrypt(sampleBytes); }); @@ -828,13 +773,12 @@ public void shouldDeleteAESKeysAndThrowOnBadPaddingExceptionWhenTryingToRSAEncry @Test public void shouldDeleteAESKeysAndThrowOnIllegalBlockSizeExceptionWhenTryingToRSAEncrypt() throws Exception { Assert.assertThrows("The RSA decrypted input is invalid.", CryptoException.class, () -> { - Certificate certificate = PowerMockito.mock(Certificate.class); - KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); + Certificate certificate = Mockito.mock(Certificate.class); + KeyStore.PrivateKeyEntry privateKeyEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); doReturn(certificate).when(privateKeyEntry).getCertificate(); doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenReturn(rsaOaepCipher); - PowerMockito.when(rsaOaepCipher.doFinal(any(byte[].class))).thenThrow(new IllegalBlockSizeException()); + Mockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenReturn(rsaOaepCipher); + Mockito.when(rsaOaepCipher.doFinal(any(byte[].class))).thenThrow(new IllegalBlockSizeException()); cryptoUtil.RSAEncrypt(new byte[0]); }); @@ -850,12 +794,11 @@ public void shouldDeleteAESKeysAndThrowOnIllegalBlockSizeExceptionWhenTryingToRS @Test public void shouldThrowOnNoSuchAlgorithmExceptionWhenTryingToRSAEncrypt() { Assert.assertThrows("The device is not compatible with the CryptoUtil class", IncompatibleDeviceException.class, () -> { - Certificate certificate = PowerMockito.mock(Certificate.class); - KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); + Certificate certificate = Mockito.mock(Certificate.class); + KeyStore.PrivateKeyEntry privateKeyEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); doReturn(certificate).when(privateKeyEntry).getCertificate(); doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenThrow(new NoSuchAlgorithmException()); + Mockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenThrow(new NoSuchAlgorithmException()); cryptoUtil.RSAEncrypt(new byte[0]); }); @@ -864,12 +807,11 @@ public void shouldThrowOnNoSuchAlgorithmExceptionWhenTryingToRSAEncrypt() { @Test public void shouldThrowOnNoSuchPaddingExceptionWhenTryingToRSAEncrypt() { Assert.assertThrows("The device is not compatible with the CryptoUtil class", IncompatibleDeviceException.class, () -> { - Certificate certificate = PowerMockito.mock(Certificate.class); - KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); + Certificate certificate = Mockito.mock(Certificate.class); + KeyStore.PrivateKeyEntry privateKeyEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); doReturn(certificate).when(privateKeyEntry).getCertificate(); doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenThrow(new NoSuchPaddingException()); + Mockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenThrow(new NoSuchPaddingException()); cryptoUtil.RSAEncrypt(new byte[0]); }); @@ -884,8 +826,8 @@ public void shouldRSADecryptData() throws Exception { byte[] sampleInput = new byte[]{0, 1, 2, 3, 4, 5}; byte[] sampleOutput = new byte[]{99, 33, 11}; - PrivateKey privateKey = PowerMockito.mock(PrivateKey.class); - KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); + PrivateKey privateKey = Mockito.mock(PrivateKey.class); + KeyStore.PrivateKeyEntry privateKeyEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); doReturn(privateKey).when(privateKeyEntry).getPrivateKey(); doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry(); doReturn(sampleOutput).when(rsaOaepCipher).doFinal(sampleInput); @@ -900,12 +842,11 @@ public void shouldRSADecryptData() throws Exception { public void shouldThrowOnInvalidKeyExceptionWhenTryingToRSADecrypt() { Assert.assertThrows("The device is not compatible with the CryptoUtil class", IncompatibleDeviceException.class, () -> { byte[] sampleBytes = new byte[0]; - PrivateKey privateKey = PowerMockito.mock(PrivateKey.class); - KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); + PrivateKey privateKey = Mockito.mock(PrivateKey.class); + KeyStore.PrivateKeyEntry privateKeyEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); doReturn(privateKey).when(privateKeyEntry).getPrivateKey(); doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenReturn(rsaOaepCipher); + Mockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenReturn(rsaOaepCipher); doThrow(new InvalidKeyException()).when(rsaOaepCipher).init(eq(Cipher.DECRYPT_MODE), eq(privateKey), any(AlgorithmParameterSpec.class)); cryptoUtil.RSADecrypt(sampleBytes); @@ -915,12 +856,11 @@ public void shouldThrowOnInvalidKeyExceptionWhenTryingToRSADecrypt() { @Test public void shouldThrowOnNoSuchAlgorithmExceptionWhenTryingToRSADecrypt() { Assert.assertThrows("The device is not compatible with the CryptoUtil class", IncompatibleDeviceException.class, () -> { - PrivateKey privateKey = PowerMockito.mock(PrivateKey.class); - KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); + PrivateKey privateKey = Mockito.mock(PrivateKey.class); + KeyStore.PrivateKeyEntry privateKeyEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); doReturn(privateKey).when(privateKeyEntry).getPrivateKey(); doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenThrow(new NoSuchAlgorithmException()); + Mockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenThrow(new NoSuchAlgorithmException()); cryptoUtil.RSADecrypt(new byte[0]); }); @@ -929,12 +869,11 @@ public void shouldThrowOnNoSuchAlgorithmExceptionWhenTryingToRSADecrypt() { @Test public void shouldThrowOnNoSuchPaddingExceptionWhenTryingToRSADecrypt() { Assert.assertThrows("The device is not compatible with the CryptoUtil class", IncompatibleDeviceException.class, () -> { - PrivateKey privateKey = PowerMockito.mock(PrivateKey.class); - KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); + PrivateKey privateKey = Mockito.mock(PrivateKey.class); + KeyStore.PrivateKeyEntry privateKeyEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); doReturn(privateKey).when(privateKeyEntry).getPrivateKey(); doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenThrow(new NoSuchPaddingException()); + Mockito.when(Cipher.getInstance(RSA_TRANSFORMATION)).thenThrow(new NoSuchPaddingException()); cryptoUtil.RSADecrypt(new byte[0]); }); @@ -943,8 +882,8 @@ public void shouldThrowOnNoSuchPaddingExceptionWhenTryingToRSADecrypt() { @Test public void shouldDeleteAESKeysAndThrowOnBadPaddingExceptionWhenTryingToRSADecrypt() throws Exception { Assert.assertThrows("The RSA encrypted input is corrupted and cannot be recovered. Please discard it.", CryptoException.class, () -> { - PrivateKey privateKey = PowerMockito.mock(PrivateKey.class); - KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); + PrivateKey privateKey = Mockito.mock(PrivateKey.class); + KeyStore.PrivateKeyEntry privateKeyEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); doReturn(privateKey).when(privateKeyEntry).getPrivateKey(); doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry(); @@ -963,8 +902,8 @@ public void shouldDeleteAESKeysAndThrowOnBadPaddingExceptionWhenTryingToRSADecry @Test public void shouldDeleteAESKeysAndThrowOnIllegalBlockSizeExceptionWhenTryingToRSADecrypt() throws Exception { Assert.assertThrows("The RSA encrypted input is corrupted and cannot be recovered. Please discard it.", CryptoException.class, () -> { - PrivateKey privateKey = PowerMockito.mock(PrivateKey.class); - KeyStore.PrivateKeyEntry privateKeyEntry = PowerMockito.mock(KeyStore.PrivateKeyEntry.class); + PrivateKey privateKey = Mockito.mock(PrivateKey.class); + KeyStore.PrivateKeyEntry privateKeyEntry = Mockito.mock(KeyStore.PrivateKeyEntry.class); doReturn(privateKey).when(privateKeyEntry).getPrivateKey(); doReturn(privateKeyEntry).when(cryptoUtil).getRSAKeyEntry(); @@ -994,8 +933,8 @@ public void shouldAESEncryptData() throws Exception { doReturn(aesKey).when(cryptoUtil).getAESKey(); doReturn(encryptedData).when(aesCipher).doFinal(data); - PowerMockito.when(aesCipher.doFinal(data)).thenReturn(encryptedData); - PowerMockito.when(aesCipher.getIV()).thenReturn(iv); + Mockito.when(aesCipher.doFinal(data)).thenReturn(encryptedData); + Mockito.when(aesCipher.getIV()).thenReturn(iv); final byte[] encrypted = cryptoUtil.encrypt(data); @@ -1006,17 +945,17 @@ public void shouldAESEncryptData() throws Exception { // IV is NO LONGER stored in storage - it's bundled with the encrypted data Mockito.verify(storage, never()).store(eq(KEY_ALIAS + "_iv"), anyString()); - + assertThat(encrypted, is(notNullValue())); assertThat(encrypted.length, is(1 + 1 + iv.length + encryptedData.length)); assertThat(encrypted[0], is((byte) 0x01)); assertThat(encrypted[1], is((byte) iv.length)); - + // Verify IV is correctly embedded byte[] extractedIV = new byte[iv.length]; System.arraycopy(encrypted, 2, extractedIV, 0, iv.length); assertThat(extractedIV, is(iv)); - + // Verify encrypted data is correctly embedded byte[] extractedEncrypted = new byte[encryptedData.length]; System.arraycopy(encrypted, 2 + iv.length, extractedEncrypted, 0, encryptedData.length); @@ -1026,9 +965,8 @@ public void shouldAESEncryptData() throws Exception { @Test public void shouldThrowOnCryptoExceptionOnRSAKeyReadingWhenTryingToAESEncrypt() { Assert.assertThrows(CryptoException.class, () -> { - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode("encoded-key", Base64.DEFAULT)).thenReturn(new byte[0]); - PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn("encoded-key"); + base64Mock.when(() -> Base64.decode("encoded-key", Base64.DEFAULT)).thenReturn(new byte[0]); + Mockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn("encoded-key"); doThrow(new CryptoException("err", null)).when(cryptoUtil).getRSAKeyEntry(); cryptoUtil.encrypt(new byte[0]); @@ -1046,9 +984,8 @@ public void shouldThrowOnCryptoExceptionOnAESKeyReadingWhenTryingToAESEncrypt() @Test public void shouldThrowOnIncompatibleDeviceExceptionOnRSAKeyReadingWhenTryingToAESEncrypt() { Assert.assertThrows("The device is not compatible with the CryptoUtil class", IncompatibleDeviceException.class, () -> { - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode("encoded-key", Base64.DEFAULT)).thenReturn(new byte[0]); - PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn("encoded-key"); + base64Mock.when(() -> Base64.decode("encoded-key", Base64.DEFAULT)).thenReturn(new byte[0]); + Mockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn("encoded-key"); doThrow(new IncompatibleDeviceException(null)).when(cryptoUtil).getRSAKeyEntry(); cryptoUtil.encrypt(new byte[0]); @@ -1069,8 +1006,7 @@ public void shouldThrowOnNoSuchPaddingExceptionWhenTryingToAESEncrypt() { Assert.assertThrows(IncompatibleDeviceException.class, () -> { doReturn(new byte[]{11, 22, 33}).when(cryptoUtil).getAESKey(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenThrow(new NoSuchPaddingException()); + Mockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenThrow(new NoSuchPaddingException()); cryptoUtil.encrypt(new byte[0]); }); @@ -1081,8 +1017,7 @@ public void shouldThrowOnNoSuchAlgorithmExceptionWhenTryingToAESEncrypt() throws Assert.assertThrows(IncompatibleDeviceException.class, () -> { doReturn(new byte[]{11, 22, 33}).when(cryptoUtil).getAESKey(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenThrow(new NoSuchAlgorithmException()); + Mockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenThrow(new NoSuchAlgorithmException()); cryptoUtil.encrypt(new byte[0]); }); @@ -1096,8 +1031,7 @@ public void shouldThrowOnInvalidKeyExceptionWhenTryingToAESEncrypt() throws Exce try { doReturn(aesKeyBytes).when(cryptoUtil).getAESKey(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); + Mockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); doThrow(new InvalidKeyException()).when(aesCipher).init(eq(Cipher.ENCRYPT_MODE), secretKeyArgumentCaptor.capture()); cryptoUtil.encrypt(new byte[0]); @@ -1160,9 +1094,9 @@ public void shouldDetectNewFormatWithValidMarkerAndIVLength12() { for (int i = 2; i < newFormatData.length; i++) { newFormatData[i] = (byte) i; } - + boolean result = cryptoUtil.isNewFormat(newFormatData); - + assertThat(result, is(true)); } @@ -1177,9 +1111,9 @@ public void shouldDetectNewFormatWithValidMarkerAndIVLength16() { for (int i = 2; i < newFormatData.length; i++) { newFormatData[i] = (byte) i; } - + boolean result = cryptoUtil.isNewFormat(newFormatData); - + assertThat(result, is(true)); } @@ -1189,9 +1123,9 @@ public void shouldNotDetectNewFormatWithInvalidMarker() { byte[] invalidData = new byte[30]; invalidData[0] = 0x02; // Wrong marker invalidData[1] = 12; // Valid IV length - + boolean result = cryptoUtil.isNewFormat(invalidData); - + assertThat(result, is(false)); } @@ -1201,9 +1135,9 @@ public void shouldNotDetectNewFormatWithInvalidIVLength() { byte[] invalidData = new byte[30]; invalidData[0] = 0x01; // Valid marker invalidData[1] = 10; // Invalid IV length (not 12 or 16) - + boolean result = cryptoUtil.isNewFormat(invalidData); - + assertThat(result, is(false)); } @@ -1211,24 +1145,24 @@ public void shouldNotDetectNewFormatWithInvalidIVLength() { public void shouldExtractIVFromNewFormatCorrectly() { byte[] iv = new byte[]{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120}; byte[] encryptedPayload = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17}; // At least 17 bytes (16 tag + 1 data) - + byte[] newFormatData = new byte[1 + 1 + iv.length + encryptedPayload.length]; newFormatData[0] = 0x01; newFormatData[1] = (byte) iv.length; System.arraycopy(iv, 0, newFormatData, 2, iv.length); System.arraycopy(encryptedPayload, 0, newFormatData, 2 + iv.length, encryptedPayload.length); - + // Verify format detection assertThat(cryptoUtil.isNewFormat(newFormatData), is(true)); - + // Manually extract and verify IV int ivLength = newFormatData[1] & 0xFF; assertThat(ivLength, is(12)); - + byte[] extractedIV = new byte[ivLength]; System.arraycopy(newFormatData, 2, extractedIV, 0, ivLength); assertThat(extractedIV, is(iv)); - + // Verify encrypted payload position int dataOffset = 2 + ivLength; int dataLength = newFormatData.length - dataOffset; @@ -1288,7 +1222,7 @@ public void shouldRejectInvalidIVLengthsInNewFormat() { byte[] ivLength255 = new byte[274]; ivLength255[0] = 0x01; - ivLength255[1] = (byte) 255; + ivLength255[1] = (byte) 255; assertThat(cryptoUtil.isNewFormat(ivLength255), is(false)); } @@ -1305,12 +1239,10 @@ public void shouldDecryptLegacyFormatDataWithIVInStorage() throws Exception { // Setup: Old format has IV stored separately in storage doReturn(aesKey).when(cryptoUtil).getAESKey(); - PowerMockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("encoded-iv-data"); - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode("encoded-iv-data", Base64.DEFAULT)).thenReturn(iv); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); - PowerMockito.when(aesCipher.doFinal(encryptedData)).thenReturn(originalData); + Mockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("encoded-iv-data"); + base64Mock.when(() -> Base64.decode("encoded-iv-data", Base64.DEFAULT)).thenReturn(iv); + Mockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); + Mockito.when(aesCipher.doFinal(encryptedData)).thenReturn(originalData); // Execute: Decrypt old format data (should be detected as legacy format) final byte[] decrypted = cryptoUtil.decrypt(encryptedData); @@ -1337,22 +1269,20 @@ public void shouldMigrateFromLegacyFormatToNewFormat() throws Exception { byte[] newIv = new byte[]{11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22}; doReturn(aesKey).when(cryptoUtil).getAESKey(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); - PowerMockito.mockStatic(Base64.class); + Mockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); // Step 1: Decrypt old format (IV from storage) - PowerMockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("old-encoded-iv"); - PowerMockito.when(Base64.decode("old-encoded-iv", Base64.DEFAULT)).thenReturn(oldIv); - PowerMockito.when(aesCipher.doFinal(oldEncryptedData)).thenReturn(originalData); + Mockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("old-encoded-iv"); + base64Mock.when(() -> Base64.decode("old-encoded-iv", Base64.DEFAULT)).thenReturn(oldIv); + Mockito.when(aesCipher.doFinal(oldEncryptedData)).thenReturn(originalData); byte[] decryptedOld = cryptoUtil.decrypt(oldEncryptedData); assertThat(decryptedOld, is(originalData)); assertThat(cryptoUtil.isNewFormat(oldEncryptedData), is(false)); // Step 2: Re-encrypt in new format (IV bundled) - PowerMockito.when(aesCipher.doFinal(originalData)).thenReturn(newEncryptedData); - PowerMockito.when(aesCipher.getIV()).thenReturn(newIv); + Mockito.when(aesCipher.doFinal(originalData)).thenReturn(newEncryptedData); + Mockito.when(aesCipher.getIV()).thenReturn(newIv); byte[] reEncrypted = cryptoUtil.encrypt(originalData); @@ -1367,7 +1297,7 @@ public void shouldMigrateFromLegacyFormatToNewFormat() throws Exception { assertThat(extractedIV, is(newIv)); // Step 3: Decrypt new format (IV bundled in data) - PowerMockito.when(aesCipher.doFinal(any(byte[].class), anyInt(), anyInt())).thenReturn(originalData); + Mockito.when(aesCipher.doFinal(any(byte[].class), anyInt(), anyInt())).thenReturn(originalData); byte[] decryptedNew = cryptoUtil.decrypt(reEncrypted); assertThat(decryptedNew, is(originalData)); @@ -1399,20 +1329,18 @@ public void shouldDecryptBothLegacyAndNewFormatInSameSession() throws Exception System.arraycopy(newEncryptedPayload, 0, newEncrypted, 2 + newIv.length, newEncryptedPayload.length); doReturn(aesKey).when(cryptoUtil).getAESKey(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); - PowerMockito.mockStatic(Base64.class); + Mockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); // Decrypt old format first - PowerMockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("old-iv-encoded"); - PowerMockito.when(Base64.decode("old-iv-encoded", Base64.DEFAULT)).thenReturn(oldIv); - PowerMockito.when(aesCipher.doFinal(oldEncrypted)).thenReturn(dataA); + Mockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("old-iv-encoded"); + base64Mock.when(() -> Base64.decode("old-iv-encoded", Base64.DEFAULT)).thenReturn(oldIv); + Mockito.when(aesCipher.doFinal(oldEncrypted)).thenReturn(dataA); byte[] decryptedOld = cryptoUtil.decrypt(oldEncrypted); assertThat(decryptedOld, is(dataA)); // Decrypt new format next - PowerMockito.when(aesCipher.doFinal(any(byte[].class), anyInt(), anyInt())).thenReturn(dataB); + Mockito.when(aesCipher.doFinal(any(byte[].class), anyInt(), anyInt())).thenReturn(dataB); byte[] decryptedNew = cryptoUtil.decrypt(newEncrypted); assertThat(decryptedNew, is(dataB)); @@ -1446,14 +1374,13 @@ public void shouldAESDecryptData() throws Exception { System.arraycopy(encryptedPayload, 0, newFormatData, 2 + iv.length, encryptedPayload.length); doReturn(aesKey).when(cryptoUtil).getAESKey(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); - PowerMockito.when(aesCipher.doFinal(any(byte[].class), anyInt(), anyInt())).thenReturn(originalData); + Mockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); + Mockito.when(aesCipher.doFinal(any(byte[].class), anyInt(), anyInt())).thenReturn(originalData); final byte[] decrypted = cryptoUtil.decrypt(newFormatData); assertThat(cryptoUtil.isNewFormat(newFormatData), is(true)); - + Mockito.verify(aesCipher).init(eq(Cipher.DECRYPT_MODE), secretKeyCaptor.capture(), ivParameterSpecCaptor.capture()); assertThat(secretKeyCaptor.getValue(), is(notNullValue())); assertThat(secretKeyCaptor.getValue().getAlgorithm(), is(ALGORITHM_AES)); @@ -1466,9 +1393,8 @@ public void shouldAESDecryptData() throws Exception { @Test public void shouldThrowOnCryptoExceptionOnRSAKeyReadingWhenTryingToAESDecrypt() { Assert.assertThrows(CryptoException.class, () -> { - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode("encoded-key", Base64.DEFAULT)).thenReturn(new byte[0]); - PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn("encoded-key"); + base64Mock.when(() -> Base64.decode("encoded-key", Base64.DEFAULT)).thenReturn(new byte[0]); + Mockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn("encoded-key"); doThrow(new CryptoException("err", null)).when(cryptoUtil).getRSAKeyEntry(); cryptoUtil.decrypt(new byte[0]); @@ -1486,9 +1412,8 @@ public void shouldThrowOnCryptoExceptionOnAESKeyReadingWhenTryingToAESDecrypt() @Test public void shouldThrowOnIncompatibleDeviceExceptionOnRSAKeyReadingWhenTryingToAESDecrypt() { Assert.assertThrows("The device is not compatible with the CryptoUtil class", IncompatibleDeviceException.class, () -> { - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode("encoded-key", Base64.DEFAULT)).thenReturn(new byte[0]); - PowerMockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn("encoded-key"); + base64Mock.when(() -> Base64.decode("encoded-key", Base64.DEFAULT)).thenReturn(new byte[0]); + Mockito.when(storage.retrieveString(KEY_ALIAS)).thenReturn("encoded-key"); doThrow(new IncompatibleDeviceException(null)).when(cryptoUtil).getRSAKeyEntry(); cryptoUtil.decrypt(new byte[0]); @@ -1508,8 +1433,7 @@ public void shouldThrowOnNoSuchPaddingExceptionWhenTryingToAESDecrypt() { Assert.assertThrows(IncompatibleDeviceException.class, () -> { doReturn(new byte[]{11, 22, 33}).when(cryptoUtil).getAESKey(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenThrow(new NoSuchPaddingException()); + Mockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenThrow(new NoSuchPaddingException()); cryptoUtil.decrypt(new byte[0]); }); @@ -1520,8 +1444,7 @@ public void shouldThrowOnNoSuchAlgorithmExceptionWhenTryingToAESDecrypt() { Assert.assertThrows(IncompatibleDeviceException.class, () -> { doReturn(new byte[]{11, 22, 33}).when(cryptoUtil).getAESKey(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenThrow(new NoSuchAlgorithmException()); + Mockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenThrow(new NoSuchAlgorithmException()); cryptoUtil.decrypt(new byte[0]); }); @@ -1531,12 +1454,11 @@ public void shouldThrowOnNoSuchAlgorithmExceptionWhenTryingToAESDecrypt() { public void shouldThrowOnEmptyInitializationVectorWhenTryingToAESDecryptWithOldFormat() { Assert.assertThrows("The encryption keys changed recently. You need to re-encrypt something first.", CryptoException.class, () -> { doReturn(new byte[]{11, 22, 33}).when(cryptoUtil).getAESKey(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); - PowerMockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn(""); - PowerMockito.when(storage.retrieveString(BASE_ALIAS + "_iv")).thenReturn(""); + Mockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); + Mockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn(""); + Mockito.when(storage.retrieveString(BASE_ALIAS + "_iv")).thenReturn(""); - cryptoUtil.decrypt(new byte[]{12,1,3,14,15,16,17}); + cryptoUtil.decrypt(new byte[]{12, 1, 3, 14, 15, 16, 17}); }); } @@ -1550,16 +1472,14 @@ public void shouldThrowOnInvalidKeyExceptionWhenTryingToAESDecrypt() throws Exce try { doReturn(aesKeyBytes).when(cryptoUtil).getAESKey(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); - PowerMockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("a_valid_iv"); + Mockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); + Mockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("a_valid_iv"); - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode("a_valid_iv", Base64.DEFAULT)).thenReturn(ivBytes); + base64Mock.when(() -> Base64.decode("a_valid_iv", Base64.DEFAULT)).thenReturn(ivBytes); doThrow(new InvalidKeyException()).when(aesCipher).init(eq(Cipher.DECRYPT_MODE), secretKeyArgumentCaptor.capture(), ivParameterSpecArgumentCaptor.capture()); - cryptoUtil.decrypt(new byte[]{12,13,14,15,16}); + cryptoUtil.decrypt(new byte[]{12, 13, 14, 15, 16}); } catch (IncompatibleDeviceException e) { exception = e; } @@ -1580,15 +1500,13 @@ public void shouldThrowOnInvalidAlgorithmParameterExceptionWhenTryingToAESDecryp try { doReturn(aesKeyBytes).when(cryptoUtil).getAESKey(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); - PowerMockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("a_valid_iv"); + Mockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); + Mockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("a_valid_iv"); - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode("a_valid_iv", Base64.DEFAULT)).thenReturn(ivBytes); + base64Mock.when(() -> Base64.decode("a_valid_iv", Base64.DEFAULT)).thenReturn(ivBytes); doThrow(new InvalidAlgorithmParameterException()).when(aesCipher).init(eq(Cipher.DECRYPT_MODE), secretKeyArgumentCaptor.capture(), ivParameterSpecArgumentCaptor.capture()); - cryptoUtil.decrypt(new byte[]{12,13,14,15,16,17}); + cryptoUtil.decrypt(new byte[]{12, 13, 14, 15, 16, 17}); } catch (IncompatibleDeviceException e) { exception = e; } @@ -1606,16 +1524,14 @@ public void shouldThrowButNotDeleteAESKeysOnBadPaddingExceptionWhenTryingToAESDe byte[] ivBytes = new byte[]{99, 22}; doReturn(aesKeyBytes).when(cryptoUtil).getAESKey(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); - PowerMockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("a_valid_iv"); + Mockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); + Mockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("a_valid_iv"); - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode("a_valid_iv", Base64.DEFAULT)).thenReturn(ivBytes); + base64Mock.when(() -> Base64.decode("a_valid_iv", Base64.DEFAULT)).thenReturn(ivBytes); doThrow(new BadPaddingException()).when(aesCipher).doFinal(any(byte[].class)); - cryptoUtil.decrypt(new byte[]{12,13,14,15,16,17}); + cryptoUtil.decrypt(new byte[]{12, 13, 14, 15, 16, 17}); }); Mockito.verify(keyStore, never()).deleteEntry(KEY_ALIAS); @@ -1631,17 +1547,15 @@ public void shouldThrowButNotDeleteAESKeysOnIllegalBlockSizeExceptionWhenTryingT Assert.assertThrows("The AES encrypted input is corrupted and cannot be recovered. Please discard it.", CryptoException.class, () -> { byte[] aesKeyBytes = new byte[]{11, 22, 33}; doReturn(aesKeyBytes).when(cryptoUtil).getAESKey(); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); - PowerMockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("a_valid_iv"); + Mockito.when(Cipher.getInstance(AES_TRANSFORMATION)).thenReturn(aesCipher); + Mockito.when(storage.retrieveString(KEY_ALIAS + "_iv")).thenReturn("a_valid_iv"); byte[] ivBytes = new byte[]{99, 22}; - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode("a_valid_iv", Base64.DEFAULT)).thenReturn(ivBytes); + base64Mock.when(() -> Base64.decode("a_valid_iv", Base64.DEFAULT)).thenReturn(ivBytes); doThrow(new IllegalBlockSizeException()).when(aesCipher).doFinal(any(byte[].class)); - cryptoUtil.decrypt(new byte[]{12,13,14,15,16,17}); + cryptoUtil.decrypt(new byte[]{12, 13, 14, 15, 16, 17}); }); Mockito.verify(keyStore, never()).deleteEntry(KEY_ALIAS); @@ -1653,66 +1567,10 @@ public void shouldThrowButNotDeleteAESKeysOnIllegalBlockSizeExceptionWhenTryingT } - /* - * Helper methods - */ - private KeyPairGeneratorSpec.Builder newKeyPairGeneratorSpecBuilder(KeyPairGeneratorSpec expectedBuilderOutput) { - KeyPairGeneratorSpec.Builder builder = PowerMockito.mock(KeyPairGeneratorSpec.Builder.class); - PowerMockito.when(builder.setAlias(anyString())).thenReturn(builder); - PowerMockito.when(builder.setSubject(any(X500Principal.class))).thenReturn(builder); - PowerMockito.when(builder.setKeySize(anyInt())).thenReturn(builder); - PowerMockito.when(builder.setSerialNumber(any(BigInteger.class))).thenReturn(builder); - PowerMockito.when(builder.setStartDate(any(Date.class))).thenReturn(builder); - PowerMockito.when(builder.setEndDate(any(Date.class))).thenReturn(builder); - PowerMockito.when(builder.setEncryptionRequired()).thenReturn(builder); - PowerMockito.when(builder.build()).thenReturn(expectedBuilderOutput); - return builder; - } - - private KeyGenParameterSpec.Builder newKeyGenParameterSpecBuilder(KeyGenParameterSpec expectedBuilderOutput) { - KeyGenParameterSpec.Builder builder = PowerMockito.mock(KeyGenParameterSpec.Builder.class); - PowerMockito.when(builder.setKeySize(anyInt())).thenReturn(builder); - PowerMockito.when(builder.setCertificateSubject(any(X500Principal.class))).thenReturn(builder); - PowerMockito.when(builder.setCertificateSerialNumber(any(BigInteger.class))).thenReturn(builder); - PowerMockito.when(builder.setCertificateNotBefore(any(Date.class))).thenReturn(builder); - PowerMockito.when(builder.setCertificateNotAfter(any(Date.class))).thenReturn(builder); - //noinspection WrongConstant - PowerMockito.when(builder.setEncryptionPaddings(anyString())).thenReturn(builder); - //noinspection WrongConstant - PowerMockito.when(builder.setDigests(anyString(), anyString())).thenReturn(builder); - //noinspection WrongConstant - PowerMockito.when(builder.setBlockModes(anyString())).thenReturn(builder); - PowerMockito.when(builder.build()).thenReturn(expectedBuilderOutput); - return builder; - } - - private CryptoUtil newCryptoUtilSpy() throws Exception { - CryptoUtil cryptoUtil = PowerMockito.spy(new CryptoUtil(context, storage, BASE_ALIAS)); - PowerMockito.mockStatic(KeyStore.class); - PowerMockito.when(KeyStore.getInstance(ANDROID_KEY_STORE)).thenReturn(keyStore); - PowerMockito.mockStatic(KeyPairGenerator.class); - PowerMockito.when(KeyPairGenerator.getInstance(ALGORITHM_RSA, ANDROID_KEY_STORE)).thenReturn(keyPairGenerator); - PowerMockito.mockStatic(KeyGenerator.class); - PowerMockito.when(KeyGenerator.getInstance(ALGORITHM_AES)).thenReturn(keyGenerator); - PowerMockito.mockStatic(Cipher.class); - PowerMockito.when(Cipher.getInstance(anyString())).then((Answer) invocation -> { - String transformation = invocation.getArgument(0, String.class); - if (RSA_TRANSFORMATION.equals(transformation)) { - return rsaOaepCipher; - } else if (OLD_RSA_PKCS1_TRANSFORMATION.equals(transformation)) { - return rsaPkcs1Cipher; - } else if (AES_TRANSFORMATION.equals(transformation)) { - return aesCipher; - } - return null; - }); - return cryptoUtil; - } - @Test public void shouldDetectAndMigratePKCS1KeyToOAEP() throws Exception { CryptoUtil cryptoUtil = newCryptoUtilSpy(); - + byte[] aesKeyBytes = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}; byte[] encryptedAESKeyPKCS1 = new byte[]{20, 21, 22, 23, 24}; byte[] encryptedAESKeyOAEP = new byte[]{30, 31, 32, 33, 34}; @@ -1721,13 +1579,12 @@ public void shouldDetectAndMigratePKCS1KeyToOAEP() throws Exception { when(storage.retrieveString(eq(KEY_ALIAS))).thenReturn(encodedEncryptedAESPKCS1); when(storage.retrieveString(eq(OLD_KEY_ALIAS))).thenReturn(null); - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode(encodedEncryptedAESPKCS1, Base64.DEFAULT)).thenReturn(encryptedAESKeyPKCS1); - PowerMockito.when(Base64.encode(encryptedAESKeyOAEP, Base64.DEFAULT)) - .thenReturn(encodedEncryptedAESOAEP.getBytes(StandardCharsets.UTF_8)); + base64Mock.when(() -> Base64.decode(encodedEncryptedAESPKCS1, Base64.DEFAULT)).thenReturn(encryptedAESKeyPKCS1); + base64Mock.when(() -> Base64.encode(encryptedAESKeyOAEP, Base64.DEFAULT)) + .thenReturn(encodedEncryptedAESOAEP.getBytes(StandardCharsets.UTF_8)); IncompatibleDeviceException incompatibleException = new IncompatibleDeviceException( - new KeyStoreException("Incompatible padding mode") + new KeyStoreException("Incompatible padding mode") ); doThrow(incompatibleException).when(cryptoUtil).RSADecrypt(encryptedAESKeyPKCS1); @@ -1740,10 +1597,10 @@ public void shouldDetectAndMigratePKCS1KeyToOAEP() throws Exception { when(mockKeyEntry.getCertificate()).thenReturn(mockCertificate); when(mockCertificate.getPublicKey()).thenReturn(mockPublicKey); when(keyStore.getEntry(eq(KEY_ALIAS), nullable(KeyStore.ProtectionParameter.class))) - .thenReturn(mockKeyEntry); + .thenReturn(mockKeyEntry); when(rsaPkcs1Cipher.doFinal(encryptedAESKeyPKCS1)).thenReturn(aesKeyBytes); - + doReturn(encryptedAESKeyOAEP).when(cryptoUtil).RSAEncrypt(aesKeyBytes); byte[] result = cryptoUtil.getAESKey(); @@ -1760,18 +1617,17 @@ public void shouldDetectAndMigratePKCS1KeyToOAEP() throws Exception { @Test public void shouldHandleKeyStoreErrorDuringMigration() throws Exception { CryptoUtil cryptoUtil = newCryptoUtilSpy(); - + String encodedEncryptedAES = "encrypted_key"; byte[] encryptedAESBytes = new byte[]{5, 6, 7, 8, 9}; - + when(storage.retrieveString(eq(KEY_ALIAS))).thenReturn(encodedEncryptedAES); when(storage.retrieveString(eq(OLD_KEY_ALIAS))).thenReturn(null); - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode(encodedEncryptedAES, Base64.DEFAULT)).thenReturn(encryptedAESBytes); + base64Mock.when(() -> Base64.decode(encodedEncryptedAES, Base64.DEFAULT)).thenReturn(encryptedAESBytes); CryptoException cryptoException = new CryptoException( - "Decryption failed", - new ProviderException("KeyStore error code -1000") + "Decryption failed", + new ProviderException("KeyStore error code -1000") ); doThrow(cryptoException).when(cryptoUtil).RSADecrypt(encryptedAESBytes); @@ -1779,12 +1635,12 @@ public void shouldHandleKeyStoreErrorDuringMigration() throws Exception { SecretKey mockSecretKey = mock(SecretKey.class); when(mockSecretKey.getEncoded()).thenReturn(newAESKey); when(keyGenerator.generateKey()).thenReturn(mockSecretKey); - + byte[] encryptedNewKey = new byte[]{30, 31, 32, 33}; doReturn(encryptedNewKey).when(cryptoUtil).RSAEncrypt(any(byte[].class)); String encodedNewKey = "new_generated_key"; - PowerMockito.when(Base64.encode(encryptedNewKey, Base64.DEFAULT)) - .thenReturn(encodedNewKey.getBytes(StandardCharsets.UTF_8)); + base64Mock.when(() -> Base64.encode(encryptedNewKey, Base64.DEFAULT)) + .thenReturn(encodedNewKey.getBytes(StandardCharsets.UTF_8)); byte[] result = cryptoUtil.getAESKey(); @@ -1797,14 +1653,13 @@ public void shouldHandleKeyStoreErrorDuringMigration() throws Exception { @Test public void shouldUseOAEPDirectlyForNewUsers() throws Exception { CryptoUtil cryptoUtil = newCryptoUtilSpy(); - + byte[] aesKeyBytes = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}; byte[] encryptedAESKeyOAEP = new byte[]{20, 21, 22, 23, 24}; String encodedEncryptedAESOAEP = "oaep_encrypted_key"; when(storage.retrieveString(eq(KEY_ALIAS))).thenReturn(encodedEncryptedAESOAEP); - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode(encodedEncryptedAESOAEP, Base64.DEFAULT)).thenReturn(encryptedAESKeyOAEP); + base64Mock.when(() -> Base64.decode(encodedEncryptedAESOAEP, Base64.DEFAULT)).thenReturn(encryptedAESKeyOAEP); doReturn(aesKeyBytes).when(cryptoUtil).RSADecrypt(encryptedAESKeyOAEP); @@ -1812,8 +1667,6 @@ public void shouldUseOAEPDirectlyForNewUsers() throws Exception { assertThat(result, is(aesKeyBytes)); - verifyPrivate(cryptoUtil).invoke("RSADecrypt", encryptedAESKeyOAEP); - Mockito.verify(rsaPkcs1Cipher, never()).init(anyInt(), any(PrivateKey.class)); Mockito.verify(rsaPkcs1Cipher, never()).doFinal(any(byte[].class)); @@ -1823,20 +1676,19 @@ public void shouldUseOAEPDirectlyForNewUsers() throws Exception { @Test public void shouldRecognizeIncompatiblePaddingModeInExceptionChain() throws Exception { CryptoUtil cryptoUtil = newCryptoUtilSpy(); - + String encodedEncryptedAES = "encrypted_key"; byte[] encryptedAESBytes = new byte[]{5, 6, 7, 8}; - + when(storage.retrieveString(eq(KEY_ALIAS))).thenReturn(encodedEncryptedAES); when(storage.retrieveString(eq(OLD_KEY_ALIAS))).thenReturn(null); - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode(encodedEncryptedAES, Base64.DEFAULT)).thenReturn(encryptedAESBytes); + base64Mock.when(() -> Base64.decode(encodedEncryptedAES, Base64.DEFAULT)).thenReturn(encryptedAESBytes); ProviderException rootCause = new ProviderException("Incompatible padding mode"); IllegalBlockSizeException middleException = new IllegalBlockSizeException("Encryption failed"); middleException.initCause(rootCause); IncompatibleDeviceException topException = new IncompatibleDeviceException(middleException); - + doThrow(topException).when(cryptoUtil).RSADecrypt(encryptedAESBytes); when(keyStore.containsAlias(KEY_ALIAS)).thenReturn(true); @@ -1844,28 +1696,28 @@ public void shouldRecognizeIncompatiblePaddingModeInExceptionChain() throws Exce PrivateKey mockPrivateKey = mock(PrivateKey.class); when(mockKeyEntry.getPrivateKey()).thenReturn(mockPrivateKey); when(keyStore.getEntry(eq(KEY_ALIAS), nullable(KeyStore.ProtectionParameter.class))) - .thenReturn(mockKeyEntry); - + .thenReturn(mockKeyEntry); + byte[] aesKeyBytes = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}; when(rsaPkcs1Cipher.doFinal(encryptedAESBytes)).thenReturn(aesKeyBytes); - + byte[] encryptedAESKeyOAEP = new byte[]{20, 21, 22, 23}; doReturn(encryptedAESKeyOAEP).when(cryptoUtil).RSAEncrypt(aesKeyBytes); String encodedOAEP = "oaep_key"; - PowerMockito.when(Base64.encode(encryptedAESKeyOAEP, Base64.DEFAULT)) - .thenReturn(encodedOAEP.getBytes(StandardCharsets.UTF_8)); + base64Mock.when(() -> Base64.encode(encryptedAESKeyOAEP, Base64.DEFAULT)) + .thenReturn(encodedOAEP.getBytes(StandardCharsets.UTF_8)); byte[] result = cryptoUtil.getAESKey(); assertThat(result, is(aesKeyBytes)); Mockito.verify(rsaPkcs1Cipher).doFinal(encryptedAESBytes); - + } @Test public void shouldAllowMultipleRetrievalsAfterMigration() throws Exception { - + CryptoUtil cryptoUtil = newCryptoUtilSpy(); - + byte[] aesKeyBytes = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}; byte[] encryptedAESKeyPKCS1 = new byte[]{20, 21, 22, 23, 24}; byte[] encryptedAESKeyOAEP = new byte[]{30, 31, 32, 33, 34}; @@ -1875,13 +1727,12 @@ public void shouldAllowMultipleRetrievalsAfterMigration() throws Exception { // First retrieval - migration happens, returns decrypted key when(storage.retrieveString(eq(KEY_ALIAS))).thenReturn(encodedEncryptedAESPKCS1); when(storage.retrieveString(eq(OLD_KEY_ALIAS))).thenReturn(null); - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode(encodedEncryptedAESPKCS1, Base64.DEFAULT)).thenReturn(encryptedAESKeyPKCS1); - PowerMockito.when(Base64.encode(encryptedAESKeyOAEP, Base64.DEFAULT)) - .thenReturn(encodedEncryptedAESOAEP.getBytes(StandardCharsets.UTF_8)); + base64Mock.when(() -> Base64.decode(encodedEncryptedAESPKCS1, Base64.DEFAULT)).thenReturn(encryptedAESKeyPKCS1); + base64Mock.when(() -> Base64.encode(encryptedAESKeyOAEP, Base64.DEFAULT)) + .thenReturn(encodedEncryptedAESOAEP.getBytes(StandardCharsets.UTF_8)); IncompatibleDeviceException incompatibleException = new IncompatibleDeviceException( - new KeyStoreException("Incompatible padding mode") + new KeyStoreException("Incompatible padding mode") ); doThrow(incompatibleException).when(cryptoUtil).RSADecrypt(encryptedAESKeyPKCS1); @@ -1894,16 +1745,16 @@ public void shouldAllowMultipleRetrievalsAfterMigration() throws Exception { when(mockKeyEntry.getCertificate()).thenReturn(mockCertificate); when(mockCertificate.getPublicKey()).thenReturn(mockPublicKey); when(keyStore.getEntry(eq(KEY_ALIAS), nullable(KeyStore.ProtectionParameter.class))) - .thenReturn(mockKeyEntry); + .thenReturn(mockKeyEntry); when(rsaPkcs1Cipher.doFinal(encryptedAESKeyPKCS1)).thenReturn(aesKeyBytes); - + // Mock RSAEncrypt for re-encrypting with OAEP after migration doReturn(encryptedAESKeyOAEP).when(cryptoUtil).RSAEncrypt(aesKeyBytes); byte[] result1 = cryptoUtil.getAESKey(); assertThat(result1, is(aesKeyBytes)); - + // Migration should delete old keys and store re-encrypted AES key Mockito.verify(keyStore).deleteEntry(KEY_ALIAS); Mockito.verify(storage).store(KEY_ALIAS, encodedEncryptedAESOAEP); @@ -1912,28 +1763,27 @@ public void shouldAllowMultipleRetrievalsAfterMigration() throws Exception { @Test public void shouldGenerateNewKeyWhenMigrationFails() throws Exception { CryptoUtil cryptoUtil = newCryptoUtilSpy(); - + String encodedOldKey = "corrupted_old_key"; byte[] encryptedOldKey = new byte[]{5, 6, 7}; - + when(storage.retrieveString(eq(KEY_ALIAS))).thenReturn(null); when(storage.retrieveString(eq(OLD_KEY_ALIAS))).thenReturn(encodedOldKey); - PowerMockito.mockStatic(Base64.class); - PowerMockito.when(Base64.decode(encodedOldKey, Base64.DEFAULT)).thenReturn(encryptedOldKey); + base64Mock.when(() -> Base64.decode(encodedOldKey, Base64.DEFAULT)).thenReturn(encryptedOldKey); doThrow(new CryptoException("Key corrupted", new KeyStoreException("Entry not found"))) - .when(cryptoUtil).getRSAKeyEntry(); + .when(cryptoUtil).getRSAKeyEntry(); byte[] newAESKey = new byte[]{21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36}; SecretKey mockSecretKey = mock(SecretKey.class); when(mockSecretKey.getEncoded()).thenReturn(newAESKey); when(keyGenerator.generateKey()).thenReturn(mockSecretKey); - + byte[] encryptedNewKey = new byte[]{40, 41, 42}; doReturn(encryptedNewKey).when(cryptoUtil).RSAEncrypt(any(byte[].class)); String encodedNewKey = "fresh_key"; - PowerMockito.when(Base64.encode(encryptedNewKey, Base64.DEFAULT)) - .thenReturn(encodedNewKey.getBytes(StandardCharsets.UTF_8)); + base64Mock.when(() -> Base64.encode(encryptedNewKey, Base64.DEFAULT)) + .thenReturn(encodedNewKey.getBytes(StandardCharsets.UTF_8)); byte[] result = cryptoUtil.getAESKey(); assertThat(result, is(newAESKey)); Mockito.verify(storage).store(KEY_ALIAS, encodedNewKey); @@ -1941,4 +1791,26 @@ public void shouldGenerateNewKeyWhenMigrationFails() throws Exception { Mockito.verify(storage, times(1)).remove(KEY_ALIAS); Mockito.verify(storage, times(1)).remove(OLD_KEY_ALIAS); } + + /* + * Helper methods + */ + private CryptoUtil newCryptoUtilSpy() throws Exception { + CryptoUtil cryptoUtil = Mockito.spy(new CryptoUtil(context, storage, BASE_ALIAS)); + Mockito.when(KeyStore.getInstance(ANDROID_KEY_STORE)).thenReturn(keyStore); + Mockito.when(KeyPairGenerator.getInstance(ALGORITHM_RSA, ANDROID_KEY_STORE)).thenReturn(keyPairGenerator); + Mockito.when(KeyGenerator.getInstance(ALGORITHM_AES)).thenReturn(keyGenerator); + Mockito.when(Cipher.getInstance(anyString())).then((Answer) invocation -> { + String transformation = invocation.getArgument(0, String.class); + if (RSA_TRANSFORMATION.equals(transformation)) { + return rsaOaepCipher; + } else if (OLD_RSA_PKCS1_TRANSFORMATION.equals(transformation)) { + return rsaPkcs1Cipher; + } else if (AES_TRANSFORMATION.equals(transformation)) { + return aesCipher; + } + return null; + }); + return cryptoUtil; + } } diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/LocalAuthenticationManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/LocalAuthenticationManagerTest.kt index 53c14baf1..91f1ea557 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/LocalAuthenticationManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/LocalAuthenticationManagerTest.kt @@ -4,10 +4,10 @@ import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.fragment.app.FragmentActivity import com.auth0.android.callback.Callback -import com.nhaarman.mockitokotlin2.KArgumentCaptor -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.verify +import org.mockito.kotlin.KArgumentCaptor +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.verify import org.hamcrest.MatcherAssert import org.hamcrest.Matchers import org.hamcrest.core.Is diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerBiometricPolicyTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerBiometricPolicyTest.kt index db478ec2e..42449325e 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerBiometricPolicyTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerBiometricPolicyTest.kt @@ -6,7 +6,7 @@ import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.callback.Callback import com.auth0.android.result.Credentials import com.auth0.android.util.Clock -import com.nhaarman.mockitokotlin2.* +import org.mockito.kotlin.* import org.junit.After import org.junit.Before import org.junit.Test diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt index 078690cc3..45fb805b2 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt @@ -22,15 +22,18 @@ import com.auth0.android.result.SSOCredentialsMock import com.auth0.android.result.toAPICredentials import com.auth0.android.util.Clock import com.google.gson.Gson -import com.nhaarman.mockitokotlin2.KArgumentCaptor -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions +import org.mockito.kotlin.KArgumentCaptor +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.verifyNoMoreInteractions import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SharedPreferencesStorageTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/SharedPreferencesStorageTest.java index 83b63210c..82fdfc210 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SharedPreferencesStorageTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SharedPreferencesStorageTest.java @@ -16,12 +16,12 @@ import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNull.nullValue; import static org.mockito.ArgumentMatchers.anySet; -import static org.mockito.Matchers.anyBoolean; -import static org.mockito.Matchers.anyFloat; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyLong; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/auth0/src/test/java/com/auth0/android/dpop/DPoPKeyStoreTest.kt b/auth0/src/test/java/com/auth0/android/dpop/DPoPKeyStoreTest.kt index c8a8c6a4c..bc0f88fb9 100644 --- a/auth0/src/test/java/com/auth0/android/dpop/DPoPKeyStoreTest.kt +++ b/auth0/src/test/java/com/auth0/android/dpop/DPoPKeyStoreTest.kt @@ -5,29 +5,29 @@ import android.content.pm.PackageManager import android.os.Build import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties -import android.util.Log -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` import org.hamcrest.Matchers.notNullValue import org.hamcrest.Matchers.nullValue +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.MockedStatic +import org.mockito.Mockito import org.mockito.Mockito.doNothing import org.mockito.Mockito.times import org.mockito.Mockito.`when` -import org.powermock.api.mockito.PowerMockito -import org.powermock.core.classloader.annotations.PrepareForTest -import org.powermock.modules.junit4.PowerMockRunner -import org.powermock.reflect.Whitebox +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config import java.security.InvalidAlgorithmParameterException import java.security.KeyPairGenerator import java.security.KeyStore @@ -36,7 +36,6 @@ import java.security.PrivateKey import java.security.ProviderException import java.security.PublicKey import java.security.cert.Certificate -import javax.security.auth.x500.X500Principal /** * Using a subclass of [DPoPKeyStore] to help with mocking the lazy initialized keyStore property @@ -45,26 +44,20 @@ internal class MockableDPoPKeyStore(private val mockKeyStore: KeyStore) : DPoPKe override val keyStore: KeyStore by lazy { mockKeyStore } } -@RunWith(PowerMockRunner::class) -@PrepareForTest( - DPoPKeyStore::class, - KeyStore::class, - KeyPairGenerator::class, - KeyGenParameterSpec.Builder::class, - Build.VERSION::class, - X500Principal::class, - Log::class -) +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.P]) public class DPoPKeyStoreTest { private lateinit var mockKeyStore: KeyStore private lateinit var mockKeyPairGenerator: KeyPairGenerator private lateinit var mockContext: Context private lateinit var mockPackageManager: PackageManager - private lateinit var mockSpecBuilder: KeyGenParameterSpec.Builder private lateinit var dpopKeyStore: DPoPKeyStore + private lateinit var keyStoreMock: MockedStatic + private lateinit var keyPairGeneratorMock: MockedStatic + @Before public fun setUp() { @@ -72,32 +65,20 @@ public class DPoPKeyStoreTest { mockKeyPairGenerator = mock() mockContext = mock() mockPackageManager = mock() - mockSpecBuilder = mock() - - PowerMockito.mockStatic(KeyStore::class.java) - PowerMockito.mockStatic(KeyPairGenerator::class.java) - PowerMockito.mockStatic(Log::class.java) - PowerMockito.mockStatic(Build.VERSION::class.java) - Whitebox.setInternalState(Build.VERSION::class.java, "SDK_INT", Build.VERSION_CODES.P) - PowerMockito.whenNew(KeyGenParameterSpec.Builder::class.java).withAnyArguments() - .thenReturn(mockSpecBuilder) + keyStoreMock = Mockito.mockStatic(KeyStore::class.java) + keyPairGeneratorMock = Mockito.mockStatic(KeyPairGenerator::class.java) - PowerMockito.`when`(KeyStore.getInstance("AndroidKeyStore")).thenReturn(mockKeyStore) + keyStoreMock.`when` { KeyStore.getInstance("AndroidKeyStore") } + .thenReturn(mockKeyStore) doNothing().whenever(mockKeyStore).load(anyOrNull()) - PowerMockito.`when`( + keyPairGeneratorMock.`when` { KeyPairGenerator.getInstance( KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore" ) - ).thenReturn(mockKeyPairGenerator) - - whenever(mockSpecBuilder.setAlgorithmParameterSpec(any())).thenReturn(mockSpecBuilder) - whenever(mockSpecBuilder.setDigests(any())).thenReturn(mockSpecBuilder) - whenever(mockSpecBuilder.setCertificateSubject(any())).thenReturn(mockSpecBuilder) - whenever(mockSpecBuilder.setCertificateNotBefore(any())).thenReturn(mockSpecBuilder) - whenever(mockSpecBuilder.setCertificateNotAfter(any())).thenReturn(mockSpecBuilder) - whenever(mockSpecBuilder.setIsStrongBoxBacked(any())).thenReturn(mockSpecBuilder) + }.thenReturn(mockKeyPairGenerator) + whenever(mockContext.packageManager).thenReturn(mockPackageManager) whenever(mockPackageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)).thenReturn( true @@ -106,6 +87,12 @@ public class DPoPKeyStoreTest { dpopKeyStore = MockableDPoPKeyStore(mockKeyStore) } + @After + public fun tearDown() { + keyStoreMock.close() + keyPairGeneratorMock.close() + } + @Test public fun `generateKeyPair should generate a key pair successfully`() { whenever(mockPackageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)).thenReturn( @@ -113,23 +100,26 @@ public class DPoPKeyStoreTest { ) dpopKeyStore.generateKeyPair(mockContext) - verify(mockKeyPairGenerator).initialize(mockSpecBuilder.build()) + verify(mockKeyPairGenerator).initialize(anyOrNull()) verify(mockKeyPairGenerator).generateKeyPair() - verify(mockSpecBuilder, never()).setIsStrongBoxBacked(true) } @Test public fun `generateKeyPair should enable StrongBox when available`() { + val specCaptor = argumentCaptor() + dpopKeyStore.generateKeyPair(mockContext) - verify(mockSpecBuilder).setIsStrongBoxBacked(true) + + verify(mockKeyPairGenerator).initialize(specCaptor.capture()) + verify(mockKeyPairGenerator).generateKeyPair() + + assertThat(specCaptor.firstValue.isStrongBoxBacked, `is`(true)) } @Test public fun `generateKeyPair should throw KEY_GENERATION_ERROR when failed to generate key pair`() { val cause = InvalidAlgorithmParameterException("Exception") - PowerMockito.`when`( - mockKeyPairGenerator.initialize(mockSpecBuilder.build()) - ).thenThrow(cause) + `when`(mockKeyPairGenerator.initialize(anyOrNull())).thenThrow(cause) val exception = assertThrows(DPoPException::class.java) { dpopKeyStore.generateKeyPair(mockContext) @@ -141,9 +131,7 @@ public class DPoPKeyStoreTest { @Test public fun `generateKeyPair should throw UNKNOWN_ERROR when any unhandled exception occurs`() { val cause = RuntimeException("Exception") - PowerMockito.`when`( - mockKeyPairGenerator.initialize(mockSpecBuilder.build()) - ).thenThrow(cause) + `when`(mockKeyPairGenerator.initialize(anyOrNull())).thenThrow(cause) val exception = assertThrows(DPoPException::class.java) { dpopKeyStore.generateKeyPair(mockContext) @@ -238,26 +226,26 @@ public class DPoPKeyStoreTest { @Test public fun `generateKeyPair should retry without StrongBox when ProviderException occurs with StrongBox enabled`() { val providerException = ProviderException("StrongBox attestation failed") + val specCaptor = argumentCaptor() `when`(mockKeyPairGenerator.generateKeyPair()).thenThrow(providerException) .thenReturn(mock()) dpopKeyStore.generateKeyPair(mockContext) - verify(mockKeyPairGenerator, times(2)).initialize(mockSpecBuilder.build()) + verify(mockKeyPairGenerator, times(2)).initialize(specCaptor.capture()) verify(mockKeyPairGenerator, times(2)).generateKeyPair() - verify(mockSpecBuilder).setIsStrongBoxBacked(true) // First attempt - verify( - mockSpecBuilder, - never() - ).setIsStrongBoxBacked(false) + assertThat(specCaptor.allValues[0].isStrongBoxBacked, `is`(true)) + assertThat(specCaptor.allValues[1].isStrongBoxBacked, `is`(false)) } @Test public fun `generateKeyPair should throw KEY_GENERATION_ERROR when ProviderException occurs without StrongBox`() { val providerException = ProviderException("Key generation failed") - `when`(mockKeyPairGenerator.initialize(mockSpecBuilder.build())).thenThrow(providerException) + `when`(mockKeyPairGenerator.initialize(anyOrNull())).thenThrow( + providerException + ) val exception = assertThrows(DPoPException::class.java) { dpopKeyStore.generateKeyPair(mockContext, useStrongBox = false) @@ -266,7 +254,7 @@ public class DPoPKeyStoreTest { assertEquals(DPoPException.KEY_GENERATION_ERROR.message, exception.message) assertThat(exception.cause, `is`(providerException)) - verify(mockKeyPairGenerator, times(1)).initialize(mockSpecBuilder.build()) + verify(mockKeyPairGenerator, times(1)).initialize(anyOrNull()) } @Test @@ -274,7 +262,7 @@ public class DPoPKeyStoreTest { val firstException = ProviderException("StrongBox failed") val secondException = ProviderException("Retry also failed") - `when`(mockKeyPairGenerator.initialize(mockSpecBuilder.build())) + `when`(mockKeyPairGenerator.initialize(anyOrNull())) .thenThrow(firstException) .thenThrow(secondException) @@ -285,6 +273,6 @@ public class DPoPKeyStoreTest { assertEquals(DPoPException.KEY_GENERATION_ERROR.message, exception.message) assertThat(exception.cause, `is`(secondException)) - verify(mockKeyPairGenerator, times(2)).initialize(mockSpecBuilder.build()) + verify(mockKeyPairGenerator, times(2)).initialize(anyOrNull()) } } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/dpop/DPoPTest.kt b/auth0/src/test/java/com/auth0/android/dpop/DPoPTest.kt index 3518e70d5..f2c45c365 100644 --- a/auth0/src/test/java/com/auth0/android/dpop/DPoPTest.kt +++ b/auth0/src/test/java/com/auth0/android/dpop/DPoPTest.kt @@ -3,9 +3,9 @@ package com.auth0.android.dpop import android.content.Context import com.auth0.android.request.HttpMethod import com.auth0.android.request.internal.Jwt -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import okhttp3.Headers import okhttp3.Response import okhttp3.ResponseBody diff --git a/auth0/src/test/java/com/auth0/android/dpop/DPoPUtilTest.kt b/auth0/src/test/java/com/auth0/android/dpop/DPoPUtilTest.kt index 1ba8050a9..19287e814 100644 --- a/auth0/src/test/java/com/auth0/android/dpop/DPoPUtilTest.kt +++ b/auth0/src/test/java/com/auth0/android/dpop/DPoPUtilTest.kt @@ -3,12 +3,15 @@ package com.auth0.android.dpop import android.content.Context import com.auth0.android.request.internal.Jwt import com.google.gson.internal.LinkedTreeMap -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions -import com.nhaarman.mockitokotlin2.whenever +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever import okhttp3.Response import org.hamcrest.CoreMatchers.`is` import org.hamcrest.CoreMatchers.notNullValue diff --git a/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt index 728e026d0..64bc7e25a 100755 --- a/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt @@ -18,7 +18,7 @@ import com.auth0.android.util.SSLTestUtils.testClient import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken -import com.nhaarman.mockitokotlin2.* +import org.mockito.kotlin.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.RecordedRequest diff --git a/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt index 342cf3b34..41d647dfd 100644 --- a/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt @@ -17,7 +17,7 @@ import com.auth0.android.util.SSLTestUtils.testClient import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken -import com.nhaarman.mockitokotlin2.mock +import org.mockito.kotlin.mock import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers diff --git a/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.kt b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.kt index b78c3aadf..ace70dd8f 100644 --- a/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/AuthenticationActivityTest.kt @@ -13,7 +13,7 @@ import com.auth0.android.provider.AuthenticationActivity.Companion.EXTRA_AUTHORI import com.auth0.android.provider.AuthenticationActivity.Companion.EXTRA_CT_OPTIONS import com.auth0.android.provider.AuthenticationActivity.Companion.EXTRA_LAUNCH_AS_TWA import com.auth0.android.provider.CustomTabsOptions -import com.nhaarman.mockitokotlin2.any +import org.mockito.kotlin.any import org.hamcrest.CoreMatchers import org.hamcrest.MatcherAssert import org.hamcrest.Matchers diff --git a/auth0/src/test/java/com/auth0/android/provider/BrowserPickerTest.java b/auth0/src/test/java/com/auth0/android/provider/BrowserPickerTest.java index 728d62f62..19704c517 100644 --- a/auth0/src/test/java/com/auth0/android/provider/BrowserPickerTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/BrowserPickerTest.java @@ -29,8 +29,8 @@ import static org.hamcrest.Matchers.isOneOf; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.IsNull.notNullValue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; diff --git a/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java b/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java index bdbfbf5f9..7d34e776c 100644 --- a/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/CustomTabsControllerTest.java @@ -1,5 +1,26 @@ package com.auth0.android.provider; +import static androidx.test.espresso.intent.matcher.IntentMatchers.hasFlag; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.Is.isA; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.ComponentName; @@ -8,7 +29,6 @@ import android.content.ServiceConnection; import android.graphics.Color; import android.net.Uri; -import android.os.Looper; import androidx.annotation.NonNull; import androidx.browser.customtabs.CustomTabsCallback; @@ -16,53 +36,27 @@ import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.CustomTabsServiceConnection; import androidx.browser.customtabs.CustomTabsSession; -import androidx.browser.trusted.TrustedWebActivityDisplayMode; import androidx.browser.trusted.TrustedWebActivityIntentBuilder; +import com.auth0.android.authentication.AuthenticationException; +import com.auth0.android.request.internal.ThreadSwitcher; +import com.google.androidbrowserhelper.trusted.TwaLauncher; +import com.google.androidbrowserhelper.trusted.splashscreens.SplashScreenStrategy; + import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import org.powermock.api.mockito.PowerMockito; +import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import java.util.List; -import static androidx.test.espresso.intent.matcher.IntentMatchers.hasFlag; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.Is.isA; -import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.auth0.android.authentication.AuthenticationException; -import com.auth0.android.request.internal.CommonThreadSwitcher; -import com.auth0.android.request.internal.ThreadSwitcher; -import com.auth0.android.util.CommonThreadSwitcherRule; -import com.google.androidbrowserhelper.trusted.TwaLauncher; -import com.google.androidbrowserhelper.trusted.splashscreens.SplashScreenStrategy; - @RunWith(RobolectricTestRunner.class) public class CustomTabsControllerTest { @@ -359,10 +353,16 @@ private void bindService(CustomTabsController controller, boolean willSucceed) { } private void connectBoundService() throws Exception { - CustomTabsSession session = mock(CustomTabsSession.class); - ComponentName componentName = new ComponentName(DEFAULT_BROWSER_PACKAGE, DEFAULT_BROWSER_PACKAGE + ".CustomTabsService"); - //This depends on an implementation detail but is the only way to test it because of methods visibility - PowerMockito.when(session, "getComponentName").thenReturn(componentName); + final ComponentName componentName = new ComponentName(DEFAULT_BROWSER_PACKAGE, DEFAULT_BROWSER_PACKAGE + ".CustomTabsService"); + + CustomTabsSession session = mock(CustomTabsSession.class, withSettings() + .strictness(Strictness.LENIENT) + .defaultAnswer((Answer) invocation -> { + if ("getComponentName".equals(invocation.getMethod().getName())) { + return componentName; + } + return null; + })); when(customTabsClient.newSession(eq(null))).thenReturn(session); CustomTabsServiceConnection conn = serviceConnectionCaptor.getValue(); diff --git a/auth0/src/test/java/com/auth0/android/provider/CustomTabsOptionsTest.java b/auth0/src/test/java/com/auth0/android/provider/CustomTabsOptionsTest.java index c4f03efb4..e6d86183d 100644 --- a/auth0/src/test/java/com/auth0/android/provider/CustomTabsOptionsTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/CustomTabsOptionsTest.java @@ -24,7 +24,7 @@ import static org.hamcrest.core.IsNull.notNullValue; import static org.hamcrest.core.IsNull.nullValue; import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; diff --git a/auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt b/auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt index e4ac82383..2cf2ec8e7 100644 --- a/auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt @@ -2,7 +2,7 @@ package com.auth0.android.provider import android.graphics.Color import com.auth0.android.Auth0 -import com.nhaarman.mockitokotlin2.mock +import org.mockito.kotlin.mock import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith diff --git a/auth0/src/test/java/com/auth0/android/provider/PKCETest.java b/auth0/src/test/java/com/auth0/android/provider/PKCETest.java index f352324fc..d6d1ad58c 100644 --- a/auth0/src/test/java/com/auth0/android/provider/PKCETest.java +++ b/auth0/src/test/java/com/auth0/android/provider/PKCETest.java @@ -29,8 +29,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/auth0/src/test/java/com/auth0/android/provider/PasskeyManagerTest.kt b/auth0/src/test/java/com/auth0/android/provider/PasskeyManagerTest.kt index 3f99b78da..f53dc7455 100644 --- a/auth0/src/test/java/com/auth0/android/provider/PasskeyManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/PasskeyManagerTest.kt @@ -28,15 +28,15 @@ 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 com.nhaarman.mockitokotlin2.KArgumentCaptor -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.doAnswer -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever +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 diff --git a/auth0/src/test/java/com/auth0/android/provider/PermissionHandlerTest.java b/auth0/src/test/java/com/auth0/android/provider/PermissionHandlerTest.java index de8ff367e..e0c44bd08 100644 --- a/auth0/src/test/java/com/auth0/android/provider/PermissionHandlerTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/PermissionHandlerTest.java @@ -21,9 +21,9 @@ import static org.hamcrest.core.IsCollectionContaining.hasItems; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index d603d3456..fceed4e59 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -27,7 +27,7 @@ import com.auth0.android.request.internal.ThreadSwitcherShadow import com.auth0.android.result.Credentials import com.auth0.android.util.AuthenticationAPIMockServer import com.auth0.android.util.SSLTestUtils -import com.nhaarman.mockitokotlin2.* +import org.mockito.kotlin.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch diff --git a/auth0/src/test/java/com/auth0/android/request/RetryInterceptorTest.kt b/auth0/src/test/java/com/auth0/android/request/RetryInterceptorTest.kt index bb10f1ce5..687f3847b 100644 --- a/auth0/src/test/java/com/auth0/android/request/RetryInterceptorTest.kt +++ b/auth0/src/test/java/com/auth0/android/request/RetryInterceptorTest.kt @@ -5,12 +5,12 @@ import com.auth0.android.dpop.DPoPKeyStore import com.auth0.android.dpop.DPoPUtil import com.auth0.android.dpop.FakeECPrivateKey import com.auth0.android.dpop.FakeECPublicKey -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever +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 +import org.mockito.kotlin.whenever import okhttp3.Interceptor import okhttp3.Protocol import okhttp3.Request diff --git a/auth0/src/test/java/com/auth0/android/request/internal/BaseAuthenticationRequestTest.kt b/auth0/src/test/java/com/auth0/android/request/internal/BaseAuthenticationRequestTest.kt index 76c9d7990..157a6d5a4 100644 --- a/auth0/src/test/java/com/auth0/android/request/internal/BaseAuthenticationRequestTest.kt +++ b/auth0/src/test/java/com/auth0/android/request/internal/BaseAuthenticationRequestTest.kt @@ -3,7 +3,7 @@ package com.auth0.android.request.internal import com.auth0.android.authentication.AuthenticationException import com.auth0.android.request.* import com.auth0.android.result.Credentials -import com.nhaarman.mockitokotlin2.* +import org.mockito.kotlin.* import org.hamcrest.CoreMatchers import org.hamcrest.MatcherAssert import org.hamcrest.collection.IsMapContaining diff --git a/auth0/src/test/java/com/auth0/android/request/internal/BaseRequestTest.kt b/auth0/src/test/java/com/auth0/android/request/internal/BaseRequestTest.kt index c459068f9..24f608345 100755 --- a/auth0/src/test/java/com/auth0/android/request/internal/BaseRequestTest.kt +++ b/auth0/src/test/java/com/auth0/android/request/internal/BaseRequestTest.kt @@ -8,7 +8,7 @@ import com.auth0.android.dpop.DPoPUtil.DPOP_HEADER import com.auth0.android.request.* import com.google.gson.Gson import com.google.gson.JsonIOException -import com.nhaarman.mockitokotlin2.* +import org.mockito.kotlin.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -199,7 +199,7 @@ public class BaseRequestTest { MatcherAssert.assertThat(exception, Matchers.`is`(wrappingAuth0Exception)) MatcherAssert.assertThat(result, Matchers.`is`(Matchers.nullValue())) verify(errorAdapter).fromException(networkError) - verifyZeroInteractions(resultAdapter) + verifyNoMoreInteractions(resultAdapter) verifyNoMoreInteractions(errorAdapter) } @@ -253,7 +253,7 @@ public class BaseRequestTest { eq(422), any() ) MatcherAssert.assertThat(wasResponseStreamClosed, Matchers.`is`(true)) - verifyZeroInteractions(resultAdapter) + verifyNoMoreInteractions(resultAdapter) verifyNoMoreInteractions(errorAdapter) } @@ -287,7 +287,7 @@ public class BaseRequestTest { ) ) MatcherAssert.assertThat(wasResponseStreamClosed, Matchers.`is`(true)) - verifyZeroInteractions(resultAdapter) + verifyNoMoreInteractions(resultAdapter) verifyNoMoreInteractions(errorAdapter) } @@ -308,7 +308,7 @@ public class BaseRequestTest { verify(resultAdapter).fromJson(any(), any()) MatcherAssert.assertThat(wasResponseStreamClosed, Matchers.`is`(true)) verifyNoMoreInteractions(resultAdapter) - verifyZeroInteractions(errorAdapter) + verifyNoMoreInteractions(errorAdapter) } @Test @@ -328,7 +328,7 @@ public class BaseRequestTest { verify(errorAdapter).fromJsonResponse(eq(422), any()) verify(errorAdapter).fromException(networkError) MatcherAssert.assertThat(wasResponseStreamClosed, Matchers.`is`(true)) - verifyZeroInteractions(resultAdapter) + verifyNoMoreInteractions(resultAdapter) verifyNoMoreInteractions(errorAdapter) } @@ -348,7 +348,7 @@ public class BaseRequestTest { MatcherAssert.assertThat(exception, Matchers.`is`(wrappingAuth0Exception)) verify(errorAdapter).fromException(networkError) MatcherAssert.assertThat(wasResponseStreamClosed, Matchers.`is`(true)) - verifyZeroInteractions(resultAdapter) + verifyNoMoreInteractions(resultAdapter) verifyNoMoreInteractions(errorAdapter) } diff --git a/auth0/src/test/java/com/auth0/android/request/internal/CommonThreadSwitcherDelegateTest.kt b/auth0/src/test/java/com/auth0/android/request/internal/CommonThreadSwitcherDelegateTest.kt index c29eccf15..cd7c1eb55 100644 --- a/auth0/src/test/java/com/auth0/android/request/internal/CommonThreadSwitcherDelegateTest.kt +++ b/auth0/src/test/java/com/auth0/android/request/internal/CommonThreadSwitcherDelegateTest.kt @@ -5,7 +5,7 @@ import com.auth0.android.callback.Callback import com.auth0.android.request.* import com.auth0.android.util.CommonThreadSwitcherRule import com.google.gson.Gson -import com.nhaarman.mockitokotlin2.* +import org.mockito.kotlin.* import org.hamcrest.MatcherAssert import org.hamcrest.Matchers import org.junit.Before diff --git a/auth0/src/test/java/com/auth0/android/request/internal/TLS12SocketFactoryTest.java b/auth0/src/test/java/com/auth0/android/request/internal/TLS12SocketFactoryTest.java index 8391bebd2..6e773ba5c 100644 --- a/auth0/src/test/java/com/auth0/android/request/internal/TLS12SocketFactoryTest.java +++ b/auth0/src/test/java/com/auth0/android/request/internal/TLS12SocketFactoryTest.java @@ -17,10 +17,10 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Matchers.anyBoolean; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/build.gradle b/build.gradle index 316d84146..4e11425c4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.8.22" + ext.kotlin_version = "2.0.21" repositories { google() mavenCentral() @@ -13,7 +13,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:7.4.0' + classpath 'com.android.tools.build:gradle:8.8.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jacoco:org.jacoco.core:0.8.5" } diff --git a/gradle.properties b/gradle.properties index 0dda7285b..1e593ff33 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,10 +21,5 @@ POM_DEVELOPER_EMAIL=oss@auth0.com org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=false # Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official - -# Adding this here temporarily to fix the build with compileSdKVersion 35. Remove this when migrate to gradle 8 -android.aapt2Version=8.6.1-11315950 \ No newline at end of file +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle index 5a1e3c327..7d341b02c 100644 --- a/gradle/jacoco.gradle +++ b/gradle/jacoco.gradle @@ -1,7 +1,7 @@ apply plugin: 'jacoco' jacoco { - toolVersion = "0.8.5" + toolVersion = "0.8.12" } android { @@ -45,8 +45,8 @@ afterEvaluate { executionData.from = "${buildDir}/jacoco/${testTaskName}.exec" reports { - xml.enabled = true - html.enabled = true + xml.required = true + html.required = true } } jacocoTestReportTask.dependsOn reportTask diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2ec77e51a..18330fcba 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/sample/build.gradle b/sample/build.gradle index 530d63778..d239bf435 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -4,11 +4,12 @@ plugins { } android { - compileSdkVersion 35 + namespace 'com.auth0.sample' + compileSdk 35 defaultConfig { - minSdkVersion 24 - targetSdkVersion 35 + minSdk 24 + targetSdk 35 versionCode 1 versionName "1.0" @@ -35,11 +36,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } } diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 8f2c85b74..8b0b759e2 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + From 451e0daa9acb574949e45780251d5765a9fd514d Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Mon, 9 Feb 2026 15:29:40 +0530 Subject: [PATCH 04/28] Resolved merge conflict --- .github/workflows/test.yml | 1 + README.md | 18 ++--- V4_MIGRATION_GUIDE.md | 71 +++++++++++++++++++ .../android/provider/WebAuthProviderTest.kt | 7 ++ 4 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 V4_MIGRATION_GUIDE.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f6a5ea5e..c523eb98c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,7 @@ on: pull_request: branches: - main + - v4_development push: branches: - main diff --git a/README.md b/README.md index 118760528..5c910adc1 100644 --- a/README.md +++ b/README.md @@ -26,21 +26,21 @@ ### Requirements -Android API version 31 or later and Java 8+. +Android API version 31 or later and Java 17+. > :warning: Applications targeting Android SDK version 30 (`targetSdkVersion = 30`) and below should use version 2.9.0. -Here’s what you need in `build.gradle` to target Java 8 byte code for Android and Kotlin plugins respectively. +Here’s what you need in `build.gradle` to target Java 17 bytecode for Android and Kotlin plugins respectively. ```groovy android { compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } } ``` @@ -52,7 +52,7 @@ To install Auth0.Android with [Gradle](https://gradle.org/), simply add the foll ```gradle dependencies { - implementation 'com.auth0.android:auth0:3.13.0' + implementation 'com.auth0.android:auth0:' } ``` @@ -117,11 +117,11 @@ Next, define the Manifest Placeholders for the Auth0 Domain and Scheme which are apply plugin: 'com.android.application' android { - compileSdkVersion 30 + compileSdkVersion 35 defaultConfig { applicationId "com.auth0.samples" - minSdkVersion 21 - targetSdkVersion 30 + minSdkVersion 24 + targetSdkVersion 35 //... //---> Add the next line diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md new file mode 100644 index 000000000..7cbe87f10 --- /dev/null +++ b/V4_MIGRATION_GUIDE.md @@ -0,0 +1,71 @@ +# Migration Guide from SDK v3 to v4 + +## Overview + +v4 of the Auth0 Android SDK includes significant build toolchain updates to support the latest Android development environment. This guide documents the changes required when migrating from v3 to v4. + +## Requirements Changes + +### Java Version + +v4 requires **Java 17** or later (previously Java 8+). + +Update your `build.gradle` to target Java 17: + +```groovy +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } +} +``` + +### Gradle and Android Gradle Plugin + +v4 requires: + +- **Gradle**: 8.10.2 or later +- **Android Gradle Plugin (AGP)**: 8.8.2 or later + +Update your `gradle/wrapper/gradle-wrapper.properties`: + +```properties +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +``` + +Update your root `build.gradle`: + +```groovy +buildscript { + dependencies { + classpath 'com.android.tools.build:gradle:8.8.2' + } +} +``` + +### Kotlin Version + +v4 uses **Kotlin 2.0.21**. If you're using Kotlin in your project, you may need to update your Kotlin version to ensure compatibility. + +```groovy +buildscript { + ext.kotlin_version = "2.0.21" +} +``` + +## Breaking Changes + +No breaking API changes have been identified in v4. This section will be updated if any are discovered. + + +## Getting Help + +If you encounter issues during migration: + +- [GitHub Issues](https://github.com/auth0/Auth0.Android/issues) - Report bugs or ask questions +- [Auth0 Community](https://community.auth0.com/) - Community support \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index fceed4e59..9eea3b9e2 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -45,6 +45,7 @@ import org.hamcrest.core.IsNot.not import org.hamcrest.core.IsNull.notNullValue import org.junit.Assert import org.junit.Before +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers @@ -1536,7 +1537,10 @@ public class WebAuthProviderTest { ) } + + // TODO: https://auth0team.atlassian.net/browse/SDK-7752 @Test + @Ignore("Fix these failing tests in CI once Roboelectric and other dependencies are updated") @Throws(Exception::class) public fun shouldFailToResumeLoginWhenRSAKeyIsMissingFromJWKSet() { val pkce = Mockito.mock(PKCE::class.java) @@ -1668,7 +1672,10 @@ public class WebAuthProviderTest { mockAPI.shutdown() } + + //TODO: https://auth0team.atlassian.net/browse/SDK-7752 @Test + @Ignore("Fix these failing tests in CI once Roboelectric and other dependencies are updated") @Throws(Exception::class) public fun shouldFailToResumeLoginWhenKeyIdIsMissingFromIdTokenHeader() { val pkce = Mockito.mock(PKCE::class.java) From e9e4e43ad3f1ce2479b652d452395de28e472790 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Tue, 10 Feb 2026 05:06:15 +0530 Subject: [PATCH 05/28] Remove deprecated jcenter() repository references --- build.gradle | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/build.gradle b/build.gradle index 4e11425c4..1324eaedd 100644 --- a/build.gradle +++ b/build.gradle @@ -4,13 +4,6 @@ buildscript { repositories { google() mavenCentral() - //noinspection JcenterRepositoryObsolete - jcenter() { - content { - // https://youtrack.jetbrains.com/issue/KT-44730 - includeModule("org.jetbrains.trove4j", "trove4j") - } - } } dependencies { classpath 'com.android.tools.build:gradle:8.8.2' @@ -48,14 +41,5 @@ allprojects { repositories { google() mavenCentral() - //noinspection JcenterRepositoryObsolete - jcenter() { - content { - // https://youtrack.jetbrains.com/issue/KT-44730 - includeModule("org.jetbrains.trove4j", "trove4j") - includeModule("com.soywiz.korlibs.korte", "korte-jvm") - includeModule("org.jetbrains.kotlinx", "kotlinx-html-jvm") - } - } } } From 52e6b73df0dc844979a123e1659fda36878915be Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Tue, 10 Feb 2026 06:10:32 +0530 Subject: [PATCH 06/28] update SDK dependencies to latest --- V4_MIGRATION_GUIDE.md | 19 ++++++++++++++++++ auth0/build.gradle | 20 +++++++++---------- .../AuthenticationAPIClientTest.kt | 14 ++++++------- .../android/management/UsersAPIClientTest.kt | 4 ++-- .../myaccount/MyAccountAPIClientTest.kt | 4 ++-- build.gradle | 2 +- 6 files changed, 41 insertions(+), 22 deletions(-) diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md index 7cbe87f10..112342aec 100644 --- a/V4_MIGRATION_GUIDE.md +++ b/V4_MIGRATION_GUIDE.md @@ -62,6 +62,25 @@ buildscript { No breaking API changes have been identified in v4. This section will be updated if any are discovered. +## Dependency Changes + +### ⚠️ Gson 2.8.9 → 2.11.0 (Transitive Dependency) + +v4 updates the internal Gson dependency from **2.8.9** to **2.11.0**. While the SDK does not expose Gson types in its public API, Gson is included as a transitive runtime dependency. If your app also uses Gson, be aware of the following changes introduced in Gson 2.10+: + +- **`TypeToken` with unresolved type variables is rejected at runtime.** Code like `object : TypeToken>() {}` (where `T` is a generic parameter) will throw `IllegalArgumentException`. Use Kotlin `reified` type parameters or pass concrete types instead. +- **Strict type coercion is enforced.** Gson no longer silently coerces JSON objects or arrays to `String`. If your code relies on this behavior, you will see `JsonSyntaxException`. +- **Built-in ProGuard/R8 rules are included.** Gson 2.11.0 ships its own keep rules, so you may be able to remove custom Gson ProGuard rules from your project. + +If you need to pin Gson to an older version, you can use Gradle's `resolutionStrategy`: + +```groovy +configurations.all { + resolutionStrategy.force 'com.google.code.gson:gson:2.8.9' +} +``` + +> **Note:** Pinning to an older version is not recommended long-term, as the SDK has been tested and validated against Gson 2.11.0. ## Getting Help diff --git a/auth0/build.gradle b/auth0/build.gradle index 9c0190136..c1c8f3d95 100644 --- a/auth0/build.gradle +++ b/auth0/build.gradle @@ -81,35 +81,35 @@ android { ext { okhttpVersion = '4.12.0' - coroutinesVersion = '1.7.3' + coroutinesVersion = '1.10.2' biometricLibraryVersion = '1.1.0' - credentialManagerVersion = "1.3.0" + credentialManagerVersion = "1.5.0" } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.6.0' - implementation 'androidx.appcompat:appcompat:1.6.0' - implementation 'androidx.browser:browser:1.4.0' + implementation 'androidx.core:core-ktx:1.15.0' + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.browser:browser:1.8.0' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" - implementation 'com.google.code.gson:gson:2.8.9' - implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.4.0' + implementation 'com.google.code.gson:gson:2.11.0' + implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.5.0' implementation "androidx.biometric:biometric:$biometricLibraryVersion" testImplementation 'junit:junit:4.13.2' testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0' - testImplementation 'org.mockito:mockito-core:5.7.0' - testImplementation 'org.mockito.kotlin:mockito-kotlin:5.1.0' + testImplementation 'org.mockito:mockito-core:5.14.0' + testImplementation 'org.mockito.kotlin:mockito-kotlin:5.4.0' testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" testImplementation "com.squareup.okhttp3:okhttp-tls:$okhttpVersion" testImplementation 'com.jayway.awaitility:awaitility:1.7.0' testImplementation 'org.robolectric:robolectric:4.14.1' - testImplementation 'androidx.test.espresso:espresso-intents:3.5.1' + testImplementation 'androidx.test.espresso:espresso-intents:3.6.1' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" testImplementation "androidx.biometric:biometric:$biometricLibraryVersion" diff --git a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt index e83b7cc16..350ec5d23 100755 --- a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt @@ -225,7 +225,7 @@ public class AuthenticationAPIClientTest { defaultLocale ) ) - val body = bodyFromRequest(request) + val body = bodyFromRequest(request) assertThat(request.path, Matchers.equalTo("/oauth/token")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -254,7 +254,7 @@ public class AuthenticationAPIClientTest { defaultLocale ) ) - val body = bodyFromRequest(request) + val body = bodyFromRequest(request) assertThat(request.path, Matchers.equalTo("/passkey/register")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("realm", MY_CONNECTION)) @@ -1034,7 +1034,7 @@ public class AuthenticationAPIClientTest { ) ) assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) - val body = bodyFromRequest(request) + val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) assertThat(body, Matchers.hasEntry("password", PASSWORD)) @@ -1058,7 +1058,7 @@ public class AuthenticationAPIClientTest { ) ) assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) - val body = bodyFromRequest(request) + val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) assertThat(body, Matchers.hasEntry("password", PASSWORD)) @@ -1361,7 +1361,7 @@ public class AuthenticationAPIClientTest { ) ) assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) - val body = bodyFromRequest(request) + val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) assertThat(body, Matchers.hasEntry("password", PASSWORD)) @@ -3132,8 +3132,8 @@ public class AuthenticationAPIClientTest { assertThat(exception.cause, Matchers.instanceOf(DPoPException::class.java)) } - private fun bodyFromRequest(request: RecordedRequest): Map { - val mapType = object : TypeToken?>() {}.type + private inline fun bodyFromRequest(request: RecordedRequest): Map { + val mapType = object : TypeToken>() {}.type return gson.fromJson(request.body.readUtf8(), mapType) } diff --git a/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt index 64bc7e25a..7c1d370e9 100755 --- a/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt @@ -441,8 +441,8 @@ public class UsersAPIClientTest { ) } - private fun bodyFromRequest(request: RecordedRequest): Map { - val mapType = object : TypeToken?>() {}.type + private inline fun bodyFromRequest(request: RecordedRequest): Map { + val mapType = object : TypeToken>() {}.type return gson.fromJson(request.body.readUtf8(), mapType) } diff --git a/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt index 41d647dfd..388530df7 100644 --- a/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt @@ -467,8 +467,8 @@ public class MyAccountAPIClientTest { assertThat(body, Matchers.hasEntry("type", "push-notification" as Any)) } - private fun bodyFromRequest(request: RecordedRequest): Map { - val mapType = object : TypeToken?>() {}.type + private inline fun bodyFromRequest(request: RecordedRequest): Map { + val mapType = object : TypeToken>() {}.type return gson.fromJson(request.body.readUtf8(), mapType) } diff --git a/build.gradle b/build.gradle index 1324eaedd..21d6f806d 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.8.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "org.jacoco:org.jacoco.core:0.8.5" + classpath "org.jacoco:org.jacoco.core:0.8.12" } } From 2402a63722a805d05fc1d9881e2789a9bb52596e Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Tue, 10 Feb 2026 12:51:16 +0530 Subject: [PATCH 07/28] -Supdate SDK dependencies to latest --- README.md | 4 ++-- V4_MIGRATION_GUIDE.md | 19 ++++++++++++++----- auth0/build.gradle | 8 ++++---- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- sample/build.gradle | 4 ++-- 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 5c910adc1..28a3baa5d 100644 --- a/README.md +++ b/README.md @@ -117,11 +117,11 @@ Next, define the Manifest Placeholders for the Auth0 Domain and Scheme which are apply plugin: 'com.android.application' android { - compileSdkVersion 35 + compileSdkVersion 36 defaultConfig { applicationId "com.auth0.samples" minSdkVersion 24 - targetSdkVersion 35 + targetSdkVersion 36 //... //---> Add the next line diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md index 112342aec..1428e630a 100644 --- a/V4_MIGRATION_GUIDE.md +++ b/V4_MIGRATION_GUIDE.md @@ -29,13 +29,13 @@ android { v4 requires: -- **Gradle**: 8.10.2 or later -- **Android Gradle Plugin (AGP)**: 8.8.2 or later +- **Gradle**: 8.11.1 or later +- **Android Gradle Plugin (AGP)**: 8.10.1 or later Update your `gradle/wrapper/gradle-wrapper.properties`: ```properties -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip ``` Update your root `build.gradle`: @@ -43,7 +43,7 @@ Update your root `build.gradle`: ```groovy buildscript { dependencies { - classpath 'com.android.tools.build:gradle:8.8.2' + classpath 'com.android.tools.build:gradle:8.10.1' } } ``` @@ -80,7 +80,16 @@ configurations.all { } ``` -> **Note:** Pinning to an older version is not recommended long-term, as the SDK has been tested and validated against Gson 2.11.0. +Alternatively, you can exclude Gson from the SDK entirely and provide your own version: + +```groovy +implementation('com.auth0.android:auth0:') { + exclude group: 'com.google.code.gson', module: 'gson' +} +implementation 'com.google.code.gson:gson:2.8.9' // your preferred version +``` + +> **Note:** Pinning or excluding is not recommended long-term, as the SDK has been tested and validated against Gson 2.11.0. ## Getting Help diff --git a/auth0/build.gradle b/auth0/build.gradle index c1c8f3d95..6226727f0 100644 --- a/auth0/build.gradle +++ b/auth0/build.gradle @@ -35,7 +35,7 @@ logger.lifecycle("Using version ${version} for ${name}") android { namespace 'com.auth0.android.auth0' - compileSdk 35 + compileSdk 36 buildFeatures { buildConfig = true @@ -43,7 +43,7 @@ android { defaultConfig { minSdkVersion 21 - targetSdk 35 + targetSdk 36 versionCode 1 versionName project.version @@ -91,7 +91,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.15.0' implementation 'androidx.appcompat:appcompat:1.7.0' - implementation 'androidx.browser:browser:1.8.0' + implementation 'androidx.browser:browser:1.9.0' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" @@ -108,7 +108,7 @@ dependencies { testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" testImplementation "com.squareup.okhttp3:okhttp-tls:$okhttpVersion" testImplementation 'com.jayway.awaitility:awaitility:1.7.0' - testImplementation 'org.robolectric:robolectric:4.14.1' + testImplementation 'org.robolectric:robolectric:4.15.1' testImplementation 'androidx.test.espresso:espresso-intents:3.6.1' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" diff --git a/build.gradle b/build.gradle index 21d6f806d..7f94c35eb 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.8.2' + classpath 'com.android.tools.build:gradle:8.10.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jacoco:org.jacoco.core:0.8.12" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 18330fcba..11581996b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/sample/build.gradle b/sample/build.gradle index d239bf435..c9f357acf 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -5,11 +5,11 @@ plugins { android { namespace 'com.auth0.sample' - compileSdk 35 + compileSdk 36 defaultConfig { minSdk 24 - targetSdk 35 + targetSdk 36 versionCode 1 versionName "1.0" From 1fb23f392dfcdfef3a0f5b97af94e72ec153b130 Mon Sep 17 00:00:00 2001 From: Prince Mathew <17837162+pmathew92@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:22:47 +0530 Subject: [PATCH 08/28] refactor: Removed the PasskeyProvider and the PasskeyManager class (#909) --- AGP_UPGRADE_PLAN.md | 399 ----------------- V4_MIGRATION_GUIDE.md | 7 +- auth0/build.gradle | 6 +- .../android/provider/PasskeyAuthProvider.kt | 234 ---------- .../auth0/android/provider/PasskeyManager.kt | 245 ---------- .../android/provider/PasskeyManagerTest.kt | 423 ------------------ proguard/proguard-jetpack.pro | 6 - sample/build.gradle | 4 +- 8 files changed, 9 insertions(+), 1315 deletions(-) delete mode 100644 AGP_UPGRADE_PLAN.md delete mode 100644 auth0/src/main/java/com/auth0/android/provider/PasskeyAuthProvider.kt delete mode 100644 auth0/src/main/java/com/auth0/android/provider/PasskeyManager.kt delete mode 100644 auth0/src/test/java/com/auth0/android/provider/PasskeyManagerTest.kt delete mode 100644 proguard/proguard-jetpack.pro 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 1428e630a..b8ca744b3 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 ## Dependency Changes diff --git a/auth0/build.gradle b/auth0/build.gradle index 6226727f0..28ff39fb9 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.10.2' biometricLibraryVersion = '1.1.0' - credentialManagerVersion = "1.5.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 c9f357acf..85612cf9a 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 From 2b2b71a1e989a7894a6bb37c7660e298fac30dd6 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Thu, 12 Feb 2026 08:45:10 +0530 Subject: [PATCH 09/28] Upgrade OkHttp to 5.0.0 and Kotlin to 2.2.0; migrate tests to mockwebserver3 --- V4_MIGRATION_GUIDE.md | 17 +- auth0/build.gradle | 4 +- .../auth0/android/provider/WebAuthProvider.kt | 4 +- .../android/request/internal/OidcUtils.kt | 2 +- .../AuthenticationAPIClientTest.kt | 496 +++++++++--------- .../authentication/MfaApiClientTest.kt | 45 +- .../android/management/UsersAPIClientTest.kt | 52 +- .../myaccount/MyAccountAPIClientTest.kt | 50 +- .../android/provider/WebAuthProviderTest.kt | 3 - .../android/request/DefaultClientTest.kt | 21 +- .../com/auth0/android/util/APIMockServer.kt | 25 +- .../util/AuthenticationAPIMockServer.kt | 14 +- .../com/auth0/android/util/SSLTestUtils.kt | 4 +- build.gradle | 2 +- 14 files changed, 377 insertions(+), 362 deletions(-) diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md index b8ca744b3..5b0e083bc 100644 --- a/V4_MIGRATION_GUIDE.md +++ b/V4_MIGRATION_GUIDE.md @@ -50,14 +50,16 @@ buildscript { ### Kotlin Version -v4 uses **Kotlin 2.0.21**. If you're using Kotlin in your project, you may need to update your Kotlin version to ensure compatibility. +v4 uses **Kotlin 2.2.0**. If you're using Kotlin in your project, you may need to update your Kotlin version to ensure compatibility. ```groovy buildscript { - ext.kotlin_version = "2.0.21" + ext.kotlin_version = "2.2.0" } ``` +> **Note:** Kotlin 2.2.0 promotes the deprecation of `String.toLowerCase()` / `String.toUpperCase()` without a `Locale` parameter to an error. If your project uses these methods, replace them with `lowercase(Locale.ROOT)` / `uppercase(Locale.ROOT)`. + ## Breaking Changes ### Classes Removed @@ -96,6 +98,17 @@ implementation 'com.google.code.gson:gson:2.8.9' // your preferred version > **Note:** Pinning or excluding is not recommended long-term, as the SDK has been tested and validated against Gson 2.11.0. +### OkHttp 4.12.0 → 5.0.0 (Internal Dependency) + +v4 upgrades the internal OkHttp dependency from **4.12.0** to **5.0.0**. OkHttp is used as an `implementation` dependency and is **not** exposed in the SDK's public API, so this change should be transparent to most applications. + +However, if your app provides a custom `NetworkingClient` implementation that interacts with OkHttp types, or if you depend on OkHttp transitively through the SDK, be aware of the following: + +- **OkHttp 5.0.0 requires Kotlin 2.2.0+** at compile time. This is already satisfied by the SDK's Kotlin version requirement. +- **Okio 3.x is now required.** OkHttp 5.0.0 depends on Okio 3.x (previously Okio 2.x). If your app uses Okio directly, you may need to update your Okio dependency. + +> **Note:** Since OkHttp is an internal dependency of the SDK, no changes are required in your application code unless you are directly depending on OkHttp types from this SDK's classpath. + ## Getting Help If you encounter issues during migration: diff --git a/auth0/build.gradle b/auth0/build.gradle index 28ff39fb9..d4769bfce 100644 --- a/auth0/build.gradle +++ b/auth0/build.gradle @@ -80,7 +80,7 @@ android { } ext { - okhttpVersion = '4.12.0' + okhttpVersion = '5.0.0' coroutinesVersion = '1.10.2' biometricLibraryVersion = '1.1.0' } @@ -104,7 +104,7 @@ dependencies { testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0' testImplementation 'org.mockito:mockito-core:5.14.0' testImplementation 'org.mockito.kotlin:mockito-kotlin:5.4.0' - testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" + testImplementation "com.squareup.okhttp3:mockwebserver3:$okhttpVersion" testImplementation "com.squareup.okhttp3:okhttp-tls:$okhttpVersion" testImplementation 'com.jayway.awaitility:awaitility:1.7.0' testImplementation 'org.robolectric:robolectric:4.15.1' diff --git a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt index 955eec9c5..09920438a 100644 --- a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt +++ b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt @@ -176,7 +176,7 @@ public object WebAuthProvider : SenderConstraining { * @return the current builder instance */ public fun withScheme(scheme: String): LogoutBuilder { - val lowerCase = scheme.toLowerCase(Locale.ROOT) + val lowerCase = scheme.lowercase(Locale.ROOT) if (scheme != lowerCase) { Log.w( TAG, @@ -397,7 +397,7 @@ public object WebAuthProvider : SenderConstraining { * @return the current builder instance */ public fun withScheme(scheme: String): Builder { - val lowerCase = scheme.toLowerCase(Locale.ROOT) + val lowerCase = scheme.lowercase(Locale.ROOT) if (scheme != lowerCase) { Log.w( TAG, diff --git a/auth0/src/main/java/com/auth0/android/request/internal/OidcUtils.kt b/auth0/src/main/java/com/auth0/android/request/internal/OidcUtils.kt index bcb22f678..ee5d2bc25 100644 --- a/auth0/src/main/java/com/auth0/android/request/internal/OidcUtils.kt +++ b/auth0/src/main/java/com/auth0/android/request/internal/OidcUtils.kt @@ -19,7 +19,7 @@ internal object OidcUtils { */ fun includeRequiredScope(scope: String): String { val existingScopes = scope.split(" ") - .map { it.toLowerCase(Locale.ROOT) } + .map { it.lowercase(Locale.ROOT) } return if (!existingScopes.contains(REQUIRED_SCOPE)) { (existingScopes + REQUIRED_SCOPE).joinToString(separator = " ").trim() } else { diff --git a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt index 350ec5d23..60a28ae9c 100755 --- a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt @@ -41,7 +41,7 @@ import org.mockito.kotlin.whenever import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import okhttp3.mockwebserver.RecordedRequest +import mockwebserver3.RecordedRequest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers import org.hamcrest.collection.IsMapContaining @@ -183,12 +183,12 @@ public class AuthenticationAPIClientTest { ) val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( body, @@ -221,12 +221,12 @@ public class AuthenticationAPIClientTest { ) val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( body, @@ -250,12 +250,12 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/passkey/register")) + assertThat(request.target, Matchers.equalTo("/passkey/register")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("realm", MY_CONNECTION)) assertThat(body, Matchers.hasEntry("organization", "testOrganization")) @@ -273,12 +273,12 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/passkey/challenge")) + assertThat(request.target, Matchers.equalTo("/passkey/challenge")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("realm", MY_CONNECTION)) assertThat(body, Matchers.hasEntry("organization", "testOrganization")) @@ -304,12 +304,12 @@ public class AuthenticationAPIClientTest { assertThat(callback.payload.recoveryCode, Matchers.`is`("654321")) val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( body, @@ -336,12 +336,12 @@ public class AuthenticationAPIClientTest { ) val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( body, @@ -369,12 +369,12 @@ public class AuthenticationAPIClientTest { ) val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( body, @@ -395,11 +395,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/mfa/challenge")) + assertThat(request.target, Matchers.equalTo("/mfa/challenge")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("mfa_token", "ey30.the-mfa-token.value")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) @@ -422,11 +422,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/mfa/challenge")) + assertThat(request.target, Matchers.equalTo("/mfa/challenge")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("mfa_token", "ey30.the-mfa-token.value")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) @@ -451,7 +451,7 @@ public class AuthenticationAPIClientTest { assertThat(credentials.user.getId(), Matchers.`is`("auth0|123456")) val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) @@ -475,7 +475,7 @@ public class AuthenticationAPIClientTest { assertThat(credentials, Matchers.`is`(Matchers.notNullValue())) val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) @@ -505,12 +505,12 @@ public class AuthenticationAPIClientTest { ) val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( body, @@ -537,9 +537,9 @@ public class AuthenticationAPIClientTest { ) ) val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.`is`("/oauth/token")) + assertThat(request.target, Matchers.`is`("/oauth/token")) assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) @@ -562,9 +562,9 @@ public class AuthenticationAPIClientTest { .execute() assertThat(credentials, Matchers.`is`(Matchers.notNullValue())) val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.`is`("/oauth/token")) + assertThat(request.target, Matchers.`is`("/oauth/token")) assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) @@ -588,9 +588,9 @@ public class AuthenticationAPIClientTest { .await() assertThat(credentials, Matchers.`is`(Matchers.notNullValue())) val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.`is`("/oauth/token")) + assertThat(request.target, Matchers.`is`("/oauth/token")) assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) @@ -619,15 +619,15 @@ public class AuthenticationAPIClientTest { ) val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) assertThat( - request.getHeader("Authorization"), + request.headers["Authorization"], Matchers.`is`("Bearer ACCESS_TOKEN") ) - assertThat(request.path, Matchers.equalTo("/userinfo")) + assertThat(request.target, Matchers.equalTo("/userinfo")) } @Test @@ -639,15 +639,15 @@ public class AuthenticationAPIClientTest { assertThat(profile, Matchers.`is`(Matchers.notNullValue())) val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) assertThat( - request.getHeader("Authorization"), + request.headers["Authorization"], Matchers.`is`("Bearer ACCESS_TOKEN") ) - assertThat(request.path, Matchers.equalTo("/userinfo")) + assertThat(request.target, Matchers.equalTo("/userinfo")) } @Test @@ -660,15 +660,15 @@ public class AuthenticationAPIClientTest { assertThat(profile, Matchers.`is`(Matchers.notNullValue())) val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) assertThat( - request.getHeader("Authorization"), + request.headers["Authorization"], Matchers.`is`("Bearer ACCESS_TOKEN") ) - assertThat(request.path, Matchers.equalTo("/userinfo")) + assertThat(request.target, Matchers.equalTo("/userinfo")) } @Test @@ -680,11 +680,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -709,11 +709,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -735,11 +735,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -763,11 +763,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -795,11 +795,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -826,11 +826,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -854,11 +854,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -882,11 +882,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -914,11 +914,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -946,11 +946,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -975,11 +975,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -1002,11 +1002,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1029,11 +1029,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1053,11 +1053,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1076,11 +1076,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1101,11 +1101,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1123,11 +1123,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1145,11 +1145,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1170,11 +1170,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1192,11 +1192,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1214,11 +1214,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1238,11 +1238,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1259,11 +1259,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1284,11 +1284,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1300,7 +1300,7 @@ public class AuthenticationAPIClientTest { ) ) val loginRequest = mockAPI.takeRequest() - assertThat(loginRequest.path, Matchers.equalTo("/oauth/token")) + assertThat(loginRequest.target, Matchers.equalTo("/oauth/token")) val loginBody = bodyFromRequest(loginRequest) assertThat(loginBody, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(loginBody, Matchers.hasEntry("password", PASSWORD)) @@ -1325,11 +1325,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1356,11 +1356,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1369,7 +1369,7 @@ public class AuthenticationAPIClientTest { assertThat(body, Matchers.hasEntry("user_metadata", testMetadata)) assertThat(credentials, Matchers.`is`(Matchers.notNullValue())) val loginRequest = mockAPI.takeRequest() - assertThat(loginRequest.path, Matchers.equalTo("/oauth/token")) + assertThat(loginRequest.target, Matchers.equalTo("/oauth/token")) val loginBody = bodyFromRequest(loginRequest) assertThat(loginBody, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(loginBody, Matchers.hasEntry("password", PASSWORD)) @@ -1392,11 +1392,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1404,7 +1404,7 @@ public class AuthenticationAPIClientTest { assertThat(body, Matchers.hasEntry("connection", MY_CONNECTION)) assertThat(credentials, Matchers.`is`(Matchers.notNullValue())) val loginRequest = mockAPI.takeRequest() - assertThat(loginRequest.path, Matchers.equalTo("/oauth/token")) + assertThat(loginRequest.target, Matchers.equalTo("/oauth/token")) val loginBody = bodyFromRequest(loginRequest) assertThat(loginBody, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(loginBody, Matchers.hasEntry("password", PASSWORD)) @@ -1429,11 +1429,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1441,7 +1441,7 @@ public class AuthenticationAPIClientTest { assertThat(body, Matchers.hasEntry("connection", MY_CONNECTION)) assertThat(credentials, Matchers.`is`(Matchers.notNullValue())) val loginRequest = mockAPI.takeRequest() - assertThat(loginRequest.path, Matchers.equalTo("/oauth/token")) + assertThat(loginRequest.target, Matchers.equalTo("/oauth/token")) val loginBody = bodyFromRequest(loginRequest) assertThat(loginBody, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(loginBody, Matchers.hasEntry("password", PASSWORD)) @@ -1465,11 +1465,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("password", PASSWORD)) @@ -1480,7 +1480,7 @@ public class AuthenticationAPIClientTest { ) ) val loginRequest = mockAPI.takeRequest() - assertThat(loginRequest.path, Matchers.equalTo("/oauth/token")) + assertThat(loginRequest.target, Matchers.equalTo("/oauth/token")) val loginBody = bodyFromRequest(loginRequest) assertThat(loginBody, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(loginBody, Matchers.hasEntry("password", PASSWORD)) @@ -1506,11 +1506,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1522,7 +1522,7 @@ public class AuthenticationAPIClientTest { ) ) val loginRequest = mockAPI.takeRequest() - assertThat(loginRequest.path, Matchers.equalTo("/oauth/token")) + assertThat(loginRequest.target, Matchers.equalTo("/oauth/token")) val loginBody = bodyFromRequest(loginRequest) assertThat(loginBody, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(loginBody, Matchers.hasEntry("password", PASSWORD)) @@ -1540,11 +1540,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) + assertThat(request.target, Matchers.equalTo("/dbconnections/change_password")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1559,11 +1559,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) + assertThat(request.target, Matchers.equalTo("/dbconnections/change_password")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1578,11 +1578,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) + assertThat(request.target, Matchers.equalTo("/dbconnections/change_password")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1598,11 +1598,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) + assertThat(request.target, Matchers.equalTo("/dbconnections/change_password")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1618,11 +1618,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) + assertThat(request.target, Matchers.equalTo("/dbconnections/change_password")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1638,11 +1638,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) + assertThat(request.target, Matchers.equalTo("/dbconnections/change_password")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1659,11 +1659,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1681,11 +1681,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1701,11 +1701,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1721,11 +1721,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1742,11 +1742,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1764,11 +1764,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1784,11 +1784,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1804,11 +1804,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1829,11 +1829,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1851,11 +1851,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1871,11 +1871,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1891,11 +1891,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1912,11 +1912,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -1934,11 +1934,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -1954,11 +1954,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -1974,11 +1974,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -1995,11 +1995,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2017,11 +2017,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2037,11 +2037,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2057,11 +2057,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2078,11 +2078,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2100,11 +2100,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2120,11 +2120,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2140,11 +2140,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2160,9 +2160,9 @@ public class AuthenticationAPIClientTest { .start(callback) ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.equalTo("/.well-known/jwks.json")) + assertThat(request.target, Matchers.equalTo("/.well-known/jwks.json")) assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) @@ -2175,9 +2175,9 @@ public class AuthenticationAPIClientTest { val result = client.fetchJsonWebKeys() .execute() val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.equalTo("/.well-known/jwks.json")) + assertThat(request.target, Matchers.equalTo("/.well-known/jwks.json")) assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) @@ -2192,9 +2192,9 @@ public class AuthenticationAPIClientTest { val result = client.fetchJsonWebKeys() .await() val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.equalTo("/.well-known/jwks.json")) + assertThat(request.target, Matchers.equalTo("/.well-known/jwks.json")) assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) @@ -2211,7 +2211,7 @@ public class AuthenticationAPIClientTest { .start(callback) ShadowLooper.idleMainLooper() val firstRequest = mockAPI.takeRequest() - assertThat(firstRequest.path, Matchers.equalTo("/oauth/token")) + assertThat(firstRequest.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(firstRequest) assertThat(body, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("password", "voidpassword")) @@ -2223,10 +2223,10 @@ public class AuthenticationAPIClientTest { ) val secondRequest = mockAPI.takeRequest() assertThat( - secondRequest.getHeader("Authorization"), + secondRequest.headers["Authorization"], Matchers.`is`("Bearer " + AuthenticationAPIMockServer.ACCESS_TOKEN) ) - assertThat(secondRequest.path, Matchers.equalTo("/userinfo")) + assertThat(secondRequest.target, Matchers.equalTo("/userinfo")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( Authentication::class.java @@ -2245,11 +2245,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/revoke")) + assertThat(request.target, Matchers.equalTo("/oauth/revoke")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("token", "refreshToken")) @@ -2265,11 +2265,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/revoke")) + assertThat(request.target, Matchers.equalTo("/oauth/revoke")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("token", "refreshToken")) @@ -2285,11 +2285,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/revoke")) + assertThat(request.target, Matchers.equalTo("/oauth/revoke")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("token", "refreshToken")) @@ -2304,11 +2304,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -2334,11 +2334,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -2361,11 +2361,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -2388,11 +2388,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -2418,11 +2418,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -2443,11 +2443,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -2473,11 +2473,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.not(Matchers.hasKey("scope"))) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) @@ -2499,11 +2499,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("refresh_token", "refreshToken")) @@ -2522,11 +2522,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("refresh_token", "refreshToken")) @@ -2545,11 +2545,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("refresh_token", "refreshToken")) @@ -2567,11 +2567,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("refresh_token", "refreshToken")) @@ -2591,11 +2591,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("refresh_token", "refreshToken")) @@ -2614,11 +2614,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.getHeader("Accept-Language"), Matchers.`is`( + request.headers["Accept-Language"], Matchers.`is`( defaultLocale ) ) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("refresh_token", "refreshToken")) @@ -2641,7 +2641,7 @@ public class AuthenticationAPIClientTest { ) .execute() val firstRequest = mockAPI.takeRequest() - assertThat(firstRequest.path, Matchers.equalTo("/oauth/token")) + assertThat(firstRequest.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(firstRequest) assertThat(body, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("password", "voidpassword")) @@ -2653,10 +2653,10 @@ public class AuthenticationAPIClientTest { ) val secondRequest = mockAPI.takeRequest() assertThat( - secondRequest.getHeader("Authorization"), + secondRequest.headers["Authorization"], Matchers.`is`("Bearer " + AuthenticationAPIMockServer.ACCESS_TOKEN) ) - assertThat(secondRequest.path, Matchers.equalTo("/userinfo")) + assertThat(secondRequest.target, Matchers.equalTo("/userinfo")) assertThat(authentication, Matchers.`is`(Matchers.notNullValue())) } @@ -2674,7 +2674,7 @@ public class AuthenticationAPIClientTest { ) .await() val firstRequest = mockAPI.takeRequest() - assertThat(firstRequest.path, Matchers.equalTo("/oauth/token")) + assertThat(firstRequest.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(firstRequest) assertThat(body, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("password", "voidpassword")) @@ -2686,10 +2686,10 @@ public class AuthenticationAPIClientTest { ) val secondRequest = mockAPI.takeRequest() assertThat( - secondRequest.getHeader("Authorization"), + secondRequest.headers["Authorization"], Matchers.`is`("Bearer " + AuthenticationAPIMockServer.ACCESS_TOKEN) ) - assertThat(secondRequest.path, Matchers.equalTo("/userinfo")) + assertThat(secondRequest.target, Matchers.equalTo("/userinfo")) assertThat(authentication, Matchers.`is`(Matchers.notNullValue())) } @@ -2702,7 +2702,7 @@ public class AuthenticationAPIClientTest { .start(callback) ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -2727,7 +2727,7 @@ public class AuthenticationAPIClientTest { .start(callback) ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -2759,8 +2759,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.nullValue()) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.headers["DPoP"], Matchers.nullValue()) + assertThat(request.target, Matchers.equalTo("/oauth/token")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( Credentials::class.java @@ -2776,8 +2776,8 @@ public class AuthenticationAPIClientTest { val challengeResponse = client.passkeyChallenge(MY_CONNECTION, "testOrganization") .execute() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.nullValue()) - assertThat(request.path, Matchers.equalTo("/passkey/challenge")) + assertThat(request.headers["DPoP"], Matchers.nullValue()) + assertThat(request.target, Matchers.equalTo("/passkey/challenge")) assertThat(challengeResponse, Matchers.`is`(Matchers.notNullValue())) } @@ -2790,8 +2790,8 @@ public class AuthenticationAPIClientTest { val challengeResponse = client.passkeyChallenge(MY_CONNECTION, "testOrganization") .execute() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.nullValue()) - assertThat(request.path, Matchers.equalTo("/passkey/challenge")) + assertThat(request.headers["DPoP"], Matchers.nullValue()) + assertThat(request.target, Matchers.equalTo("/passkey/challenge")) assertThat(challengeResponse, Matchers.`is`(Matchers.notNullValue())) } @@ -2808,8 +2808,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.notNullValue()) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.headers["DPoP"], Matchers.notNullValue()) + assertThat(request.target, Matchers.equalTo("/oauth/token")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( Credentials::class.java @@ -2829,8 +2829,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.nullValue()) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.headers["DPoP"], Matchers.nullValue()) + assertThat(request.target, Matchers.equalTo("/oauth/token")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( Credentials::class.java @@ -2848,8 +2848,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.nullValue()) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.headers["DPoP"], Matchers.nullValue()) + assertThat(request.target, Matchers.equalTo("/oauth/token")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( Credentials::class.java @@ -2871,8 +2871,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.notNullValue()) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.headers["DPoP"], Matchers.notNullValue()) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( @@ -2906,8 +2906,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.nullValue()) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.headers["DPoP"], Matchers.nullValue()) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("grant_type", "refresh_token")) @@ -2938,8 +2938,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.notNullValue()) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.headers["DPoP"], Matchers.notNullValue()) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( @@ -2973,9 +2973,9 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.notNullValue()) - assertThat(request.getHeader("Authorization"), Matchers.`is`("DPoP ACCESS_TOKEN")) - assertThat(request.path, Matchers.equalTo("/userinfo")) + assertThat(request.headers["DPoP"], Matchers.notNullValue()) + assertThat(request.headers["Authorization"], Matchers.`is`("DPoP ACCESS_TOKEN")) + assertThat(request.target, Matchers.equalTo("/userinfo")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( UserProfile::class.java @@ -2993,9 +2993,9 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.nullValue()) - assertThat(request.getHeader("Authorization"), Matchers.`is`("Bearer ACCESS_TOKEN")) - assertThat(request.path, Matchers.equalTo("/userinfo")) + assertThat(request.headers["DPoP"], Matchers.nullValue()) + assertThat(request.headers["Authorization"], Matchers.`is`("Bearer ACCESS_TOKEN")) + assertThat(request.target, Matchers.equalTo("/userinfo")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( UserProfile::class.java @@ -3017,8 +3017,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.nullValue()) - assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.headers["DPoP"], Matchers.nullValue()) + assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( DatabaseUser::class.java @@ -3040,8 +3040,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.nullValue()) - assertThat(request.path, Matchers.equalTo("/passwordless/start")) + assertThat(request.headers["DPoP"], Matchers.nullValue()) + assertThat(request.target, Matchers.equalTo("/passwordless/start")) assertThat(callback, AuthenticationCallbackMatcher.hasNoError()) } @@ -3059,8 +3059,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.nullValue()) - assertThat(request.path, Matchers.equalTo("/.well-known/jwks.json")) + assertThat(request.headers["DPoP"], Matchers.nullValue()) + assertThat(request.target, Matchers.equalTo("/.well-known/jwks.json")) assertThat(callback, AuthenticationCallbackMatcher.hasPayload(emptyMap())) } @@ -3077,8 +3077,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.notNullValue()) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.headers["DPoP"], Matchers.notNullValue()) + assertThat(request.target, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -3104,8 +3104,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.getHeader("DPoP"), Matchers.notNullValue()) - assertThat(request.path, Matchers.equalTo("/oauth/token")) + assertThat(request.headers["DPoP"], Matchers.notNullValue()) + assertThat(request.target, Matchers.equalTo("/oauth/token")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( SSOCredentials::class.java @@ -3134,7 +3134,7 @@ public class AuthenticationAPIClientTest { private inline fun bodyFromRequest(request: RecordedRequest): Map { val mapType = object : TypeToken>() {}.type - return gson.fromJson(request.body.readUtf8(), mapType) + return gson.fromJson(request.body!!.utf8(), mapType) } private val defaultLocale: String diff --git a/auth0/src/test/java/com/auth0/android/authentication/MfaApiClientTest.kt b/auth0/src/test/java/com/auth0/android/authentication/MfaApiClientTest.kt index 8fec80b5f..8090187bd 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/MfaApiClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/MfaApiClientTest.kt @@ -20,9 +20,9 @@ import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.MockWebServer -import okhttp3.mockwebserver.RecordedRequest +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import mockwebserver3.RecordedRequest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.* import org.junit.After @@ -57,15 +57,16 @@ public class MfaApiClientTest { @After public fun tearDown(): Unit { - mockServer.shutdown() + mockServer.close() } private fun enqueueMockResponse(json: String, statusCode: Int = 200): Unit { mockServer.enqueue( - MockResponse() - .setResponseCode(statusCode) + MockResponse.Builder() + .code(statusCode) .addHeader("Content-Type", "application/json") - .setBody(json) + .body(json) + .build() ) } @@ -76,7 +77,7 @@ public class MfaApiClientTest { private inline fun bodyFromRequest(request: RecordedRequest): Map { val mapType = object : TypeToken>() {}.type - return gson.fromJson(request.body.readUtf8(), mapType) + return gson.fromJson(request.body!!.utf8(), mapType) } @@ -139,8 +140,8 @@ public class MfaApiClientTest { mfaClient.getAuthenticators(listOf("oob")).await() val request = mockServer.takeRequest() - assertThat(request.getHeader("Authorization"), `is`("Bearer $MFA_TOKEN")) - assertThat(request.path, `is`("/mfa/authenticators")) + assertThat(request.headers["Authorization"], `is`("Bearer $MFA_TOKEN")) + assertThat(request.target, `is`("/mfa/authenticators")) assertThat(request.method, `is`("GET")) } @@ -193,9 +194,9 @@ public class MfaApiClientTest { mfaClient.enroll(MfaEnrollmentType.Phone("+12025550135")).await() val request = mockServer.takeRequest() - assertThat(request.path, `is`("/mfa/associate")) + assertThat(request.target, `is`("/mfa/associate")) assertThat(request.method, `is`("POST")) - assertThat(request.getHeader("Authorization"), `is`("Bearer $MFA_TOKEN")) + assertThat(request.headers["Authorization"], `is`("Bearer $MFA_TOKEN")) val body = bodyFromRequest(request) assertThat(body["authenticator_types"], `is`(listOf("oob"))) @@ -240,9 +241,9 @@ public class MfaApiClientTest { mfaClient.enroll(MfaEnrollmentType.Email("user@example.com")).await() val request = mockServer.takeRequest() - assertThat(request.path, `is`("/mfa/associate")) + assertThat(request.target, `is`("/mfa/associate")) assertThat(request.method, `is`("POST")) - assertThat(request.getHeader("Authorization"), `is`("Bearer $MFA_TOKEN")) + assertThat(request.headers["Authorization"], `is`("Bearer $MFA_TOKEN")) val body = bodyFromRequest(request) assertThat(body["authenticator_types"], `is`(listOf("oob"))) @@ -297,9 +298,9 @@ public class MfaApiClientTest { mfaClient.enroll(MfaEnrollmentType.Otp).await() val request = mockServer.takeRequest() - assertThat(request.path, `is`("/mfa/associate")) + assertThat(request.target, `is`("/mfa/associate")) assertThat(request.method, `is`("POST")) - assertThat(request.getHeader("Authorization"), `is`("Bearer $MFA_TOKEN")) + assertThat(request.headers["Authorization"], `is`("Bearer $MFA_TOKEN")) val body = bodyFromRequest(request) assertThat(body["authenticator_types"], `is`(listOf("otp"))) @@ -342,9 +343,9 @@ public class MfaApiClientTest { mfaClient.enroll(MfaEnrollmentType.Push).await() val request = mockServer.takeRequest() - assertThat(request.path, `is`("/mfa/associate")) + assertThat(request.target, `is`("/mfa/associate")) assertThat(request.method, `is`("POST")) - assertThat(request.getHeader("Authorization"), `is`("Bearer $MFA_TOKEN")) + assertThat(request.headers["Authorization"], `is`("Bearer $MFA_TOKEN")) val body = bodyFromRequest(request) assertThat(body["authenticator_types"], `is`(listOf("oob"))) @@ -390,7 +391,7 @@ public class MfaApiClientTest { mfaClient.challenge("sms|dev_123").await() val request = mockServer.takeRequest() - assertThat(request.path, `is`("/mfa/challenge")) + assertThat(request.target, `is`("/mfa/challenge")) assertThat(request.method, `is`("POST")) val body = bodyFromRequest(request) @@ -442,7 +443,7 @@ public class MfaApiClientTest { mfaClient.verify(MfaVerificationType.Otp("123456")).await() val request = mockServer.takeRequest() - assertThat(request.path, `is`("/oauth/token")) + assertThat(request.target, `is`("/oauth/token")) assertThat(request.method, `is`("POST")) val body = bodyFromRequest(request) @@ -517,7 +518,7 @@ public class MfaApiClientTest { mfaClient.verify(MfaVerificationType.Oob(oobCode = "oob_code_123", bindingCode = "654321")).await() val request = mockServer.takeRequest() - assertThat(request.path, `is`("/oauth/token")) + assertThat(request.target, `is`("/oauth/token")) assertThat(request.method, `is`("POST")) val body = bodyFromRequest(request) @@ -580,7 +581,7 @@ public class MfaApiClientTest { mfaClient.verify(MfaVerificationType.RecoveryCode("RECOVERY_123")).await() val request = mockServer.takeRequest() - assertThat(request.path, `is`("/oauth/token")) + assertThat(request.target, `is`("/oauth/token")) assertThat(request.method, `is`("POST")) val body = bodyFromRequest(request) diff --git a/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt index 7c1d370e9..06a11d4ee 100755 --- a/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt @@ -21,7 +21,7 @@ import com.google.gson.reflect.TypeToken import org.mockito.kotlin.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import okhttp3.mockwebserver.RecordedRequest +import mockwebserver3.RecordedRequest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers import org.hamcrest.Matchers.instanceOf @@ -143,11 +143,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.path, + request.target, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities") ) assertThat( - request.getHeader(HEADER_AUTHORIZATION), + request.headers[HEADER_AUTHORIZATION], Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_POST)) @@ -168,11 +168,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.path, + request.target, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities") ) assertThat( - request.getHeader(HEADER_AUTHORIZATION), + request.headers[HEADER_AUTHORIZATION], Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_POST)) @@ -194,11 +194,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.path, + request.target, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities") ) assertThat( - request.getHeader(HEADER_AUTHORIZATION), + request.headers[HEADER_AUTHORIZATION], Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_POST)) @@ -222,11 +222,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.path, + request.target, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities/$PROVIDER/$USER_ID_SECONDARY") ) assertThat( - request.getHeader(HEADER_AUTHORIZATION), + request.headers[HEADER_AUTHORIZATION], Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_DELETE)) @@ -247,11 +247,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.path, + request.target, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities/$PROVIDER/$USER_ID_SECONDARY") ) assertThat( - request.getHeader(HEADER_AUTHORIZATION), + request.headers[HEADER_AUTHORIZATION], Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_DELETE)) @@ -273,11 +273,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.path, + request.target, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities/$PROVIDER/$USER_ID_SECONDARY") ) assertThat( - request.getHeader(HEADER_AUTHORIZATION), + request.headers[HEADER_AUTHORIZATION], Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_DELETE)) @@ -303,9 +303,9 @@ public class UsersAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY")) + assertThat(request.target, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY")) assertThat( - request.getHeader(HEADER_AUTHORIZATION), + request.headers[HEADER_AUTHORIZATION], Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_PATCH)) @@ -332,11 +332,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.path, + request.target, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY") ) assertThat( - request.getHeader(HEADER_AUTHORIZATION), + request.headers[HEADER_AUTHORIZATION], Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_PATCH)) @@ -361,11 +361,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.path, + request.target, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY") ) assertThat( - request.getHeader(HEADER_AUTHORIZATION), + request.headers[HEADER_AUTHORIZATION], Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_PATCH)) @@ -384,11 +384,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.path, + request.target, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY") ) assertThat( - request.getHeader(HEADER_AUTHORIZATION), + request.headers[HEADER_AUTHORIZATION], Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat( @@ -407,11 +407,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.path, + request.target, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY") ) assertThat( - request.getHeader(HEADER_AUTHORIZATION), + request.headers[HEADER_AUTHORIZATION], Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat( @@ -430,8 +430,8 @@ public class UsersAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY")) - assertThat(request.getHeader(HEADER_AUTHORIZATION), + assertThat(request.target, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY")) + assertThat(request.headers[HEADER_AUTHORIZATION], Matchers.equalTo(BEARER + TOKEN_PRIMARY)) assertThat(request.method, Matchers.equalTo(METHOD_GET)) assertThat( @@ -443,7 +443,7 @@ public class UsersAPIClientTest { private inline fun bodyFromRequest(request: RecordedRequest): Map { val mapType = object : TypeToken>() {}.type - return gson.fromJson(request.body.readUtf8(), mapType) + return gson.fromJson(request.body!!.utf8(), mapType) } private companion object { diff --git a/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt index 388530df7..499cf1cd4 100644 --- a/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt @@ -18,7 +18,7 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken import org.mockito.kotlin.mock -import okhttp3.mockwebserver.RecordedRequest +import mockwebserver3.RecordedRequest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers import org.junit.After @@ -56,7 +56,7 @@ public class MyAccountAPIClientTest { client.passkeyEnrollmentChallenge() .start(callback) val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods")) + assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods")) } @Test @@ -92,7 +92,7 @@ public class MyAccountAPIClientTest { .start(callback) val request = mockAPI.takeRequest() - val header = request.getHeader("Authorization") + val header = request.headers["Authorization"] assertThat( header, Matchers.`is`( @@ -147,7 +147,7 @@ public class MyAccountAPIClientTest { // Take and verify the request was sent correctly val request = mockAPI.takeRequest() assertThat( - request.path, + request.target, Matchers.equalTo("/me/v1/authentication-methods") ) // Verify error details @@ -175,7 +175,7 @@ public class MyAccountAPIClientTest { } val request = mockAPI.takeRequest() assertThat( - request.path, + request.target, Matchers.equalTo("/me/v1/authentication-methods") ) @@ -206,7 +206,7 @@ public class MyAccountAPIClientTest { .start(callback) val request = mockAPI.takeRequest() assertThat( - request.path, + request.target, Matchers.equalTo("/me/v1/authentication-methods/${AUTHENTICATION_ID}/verify") ) } @@ -248,7 +248,7 @@ public class MyAccountAPIClientTest { .start(callback) val request = mockAPI.takeRequest() - val header = request.getHeader("Authorization") + val header = request.headers["Authorization"] assertThat( header, Matchers.`is`( @@ -299,7 +299,7 @@ public class MyAccountAPIClientTest { // Take and verify the request was sent correctly val request = mockAPI.takeRequest() assertThat( - request.path, + request.target, Matchers.equalTo("/me/v1/authentication-methods/${AUTHENTICATION_ID}/verify") ) assertThat(error, Matchers.notNullValue()) @@ -318,8 +318,8 @@ public class MyAccountAPIClientTest { client.getFactors().start(callback) val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.equalTo("/me/v1/factors")) - assertThat(request.getHeader("Authorization"), Matchers.equalTo("Bearer $ACCESS_TOKEN")) + assertThat(request.target, Matchers.equalTo("/me/v1/factors")) + assertThat(request.headers["Authorization"], Matchers.equalTo("Bearer $ACCESS_TOKEN")) assertThat(request.method, Matchers.equalTo("GET")) } @@ -329,8 +329,8 @@ public class MyAccountAPIClientTest { client.getAuthenticationMethods().start(callback) val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods")) - assertThat(request.getHeader("Authorization"), Matchers.equalTo("Bearer $ACCESS_TOKEN")) + assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods")) + assertThat(request.headers["Authorization"], Matchers.equalTo("Bearer $ACCESS_TOKEN")) assertThat(request.method, Matchers.equalTo("GET")) } @@ -341,8 +341,8 @@ public class MyAccountAPIClientTest { client.getAuthenticationMethodById(methodId).start(callback) val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods/email%7C12345")) - assertThat(request.getHeader("Authorization"), Matchers.equalTo("Bearer $ACCESS_TOKEN")) + assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods/email%7C12345")) + assertThat(request.headers["Authorization"], Matchers.equalTo("Bearer $ACCESS_TOKEN")) assertThat(request.method, Matchers.equalTo("GET")) } @@ -353,8 +353,8 @@ public class MyAccountAPIClientTest { client.deleteAuthenticationMethod(methodId).start(callback) val request = mockAPI.takeRequest() - assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods/email%7C12345")) - assertThat(request.getHeader("Authorization"), Matchers.equalTo("Bearer $ACCESS_TOKEN")) + assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods/email%7C12345")) + assertThat(request.headers["Authorization"], Matchers.equalTo("Bearer $ACCESS_TOKEN")) assertThat(request.method, Matchers.equalTo("DELETE")) } @@ -366,7 +366,7 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods/phone%7C12345")) + assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods/phone%7C12345")) assertThat(request.method, Matchers.equalTo("PATCH")) assertThat(body, Matchers.hasEntry("preferred_authentication_method", "sms" as Any)) } @@ -380,7 +380,7 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods/totp%7C12345")) + assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods/totp%7C12345")) assertThat(request.method, Matchers.equalTo("PATCH")) assertThat(body, Matchers.hasEntry("name", name as Any)) } @@ -393,7 +393,7 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods")) + assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods")) assertThat(request.method, Matchers.equalTo("POST")) assertThat(body, Matchers.hasEntry("type", "email" as Any)) assertThat(body, Matchers.hasEntry("email", email as Any)) @@ -407,7 +407,7 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods")) + assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods")) assertThat(request.method, Matchers.equalTo("POST")) assertThat(body, Matchers.hasEntry("type", "phone" as Any)) assertThat(body, Matchers.hasEntry("phone_number", phoneNumber as Any)) @@ -421,7 +421,7 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods")) + assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods")) assertThat(request.method, Matchers.equalTo("POST")) assertThat(body, Matchers.hasEntry("type", "totp" as Any)) } @@ -434,7 +434,7 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods")) + assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods")) assertThat(request.method, Matchers.equalTo("POST")) assertThat(body, Matchers.hasEntry("type", "recovery-code" as Any)) } @@ -449,7 +449,7 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods/email%7C123/verify")) + assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods/email%7C123/verify")) assertThat(request.method, Matchers.equalTo("POST")) assertThat(body, Matchers.hasEntry("otp_code", otp as Any)) assertThat(body, Matchers.hasEntry("auth_session", session as Any)) @@ -462,14 +462,14 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods")) + assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods")) assertThat(request.method, Matchers.equalTo("POST")) assertThat(body, Matchers.hasEntry("type", "push-notification" as Any)) } private inline fun bodyFromRequest(request: RecordedRequest): Map { val mapType = object : TypeToken>() {}.type - return gson.fromJson(request.body.readUtf8(), mapType) + return gson.fromJson(request.body!!.utf8(), mapType) } private val auth0: Auth0 diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index 9eea3b9e2..aa619ecc1 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -45,7 +45,6 @@ import org.hamcrest.core.IsNot.not import org.hamcrest.core.IsNull.notNullValue import org.junit.Assert import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers @@ -1540,7 +1539,6 @@ public class WebAuthProviderTest { // TODO: https://auth0team.atlassian.net/browse/SDK-7752 @Test - @Ignore("Fix these failing tests in CI once Roboelectric and other dependencies are updated") @Throws(Exception::class) public fun shouldFailToResumeLoginWhenRSAKeyIsMissingFromJWKSet() { val pkce = Mockito.mock(PKCE::class.java) @@ -1675,7 +1673,6 @@ public class WebAuthProviderTest { //TODO: https://auth0team.atlassian.net/browse/SDK-7752 @Test - @Ignore("Fix these failing tests in CI once Roboelectric and other dependencies are updated") @Throws(Exception::class) public fun shouldFailToResumeLoginWhenKeyIdIsMissingFromIdTokenHeader() { val pkce = Mockito.mock(PKCE::class.java) diff --git a/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt b/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt index 7a71a70bf..b02755f11 100644 --- a/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt @@ -6,9 +6,9 @@ import com.google.gson.Gson import com.google.gson.reflect.TypeToken import okhttp3.Interceptor import okhttp3.logging.HttpLoggingInterceptor -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.MockWebServer -import okhttp3.mockwebserver.RecordedRequest +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import mockwebserver3.RecordedRequest import org.hamcrest.CoreMatchers.* import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.empty @@ -51,7 +51,7 @@ public class DefaultClientTest { @After public fun tearDown() { - mockServer.shutdown() + mockServer.close() } @Test @@ -274,7 +274,7 @@ public class DefaultClientTest { method: HttpMethod, headers: Map = mapOf("a-header" to "b-value") ) { - val requestUri = Uri.parse(request.path) + val requestUri = Uri.parse(request.target) when (method) { HttpMethod.GET -> assertThat(request.method, equalTo("GET")) HttpMethod.POST -> assertThat(request.method, equalTo("POST")) @@ -331,10 +331,11 @@ public class DefaultClientTest { } private fun enqueueMockResponse(responseCode: Int = STATUS_SUCCESS, jsonBody: String) { - val response = MockResponse() - response.setBody(jsonBody) - response.setResponseCode(responseCode) - response.setHeader("content-type", "application/json") + val response = MockResponse.Builder() + .body(jsonBody) + .code(responseCode) + .setHeader("content-type", "application/json") + .build() mockServer.enqueue(response) } @@ -344,7 +345,7 @@ public class DefaultClientTest { .collect(Collectors.joining("\n")) private fun RecordedRequest.bodyFromJson(): Map { - val text = this.body.readUtf8() + val text = this.body?.utf8() ?: "" if (text.isEmpty()) { return emptyMap() } diff --git a/auth0/src/test/java/com/auth0/android/util/APIMockServer.kt b/auth0/src/test/java/com/auth0/android/util/APIMockServer.kt index de8ddd305..23f808e31 100644 --- a/auth0/src/test/java/com/auth0/android/util/APIMockServer.kt +++ b/auth0/src/test/java/com/auth0/android/util/APIMockServer.kt @@ -1,8 +1,8 @@ package com.auth0.android.util -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.MockWebServer -import okhttp3.mockwebserver.RecordedRequest +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import mockwebserver3.RecordedRequest import java.io.IOException internal abstract class APIMockServer { @@ -12,7 +12,7 @@ internal abstract class APIMockServer { @Throws(IOException::class) fun shutdown() { - server.shutdown() + server.close() } @Throws(InterruptedException::class) @@ -21,22 +21,23 @@ internal abstract class APIMockServer { } fun responseWithJSON(json: String, statusCode: Int): MockResponse { - return MockResponse() - .setResponseCode(statusCode) + return MockResponse.Builder() + .code(statusCode) .addHeader("Content-Type", "application/json") - .setBody(json) + .body(json) + .build() } fun responseWithJSON(json: String, statusCode: Int, header: Map): MockResponse { - val response = MockResponse() - .setResponseCode(statusCode) + val builder = MockResponse.Builder() + .code(statusCode) .addHeader("Content-Type", "application/json") - .setBody(json) + .body(json) header.forEach { (key, value) -> - response.addHeader(key, value) + builder.addHeader(key, value) } - return response + return builder.build() } init { diff --git a/auth0/src/test/java/com/auth0/android/util/AuthenticationAPIMockServer.kt b/auth0/src/test/java/com/auth0/android/util/AuthenticationAPIMockServer.kt index e08361e57..10375ec90 100755 --- a/auth0/src/test/java/com/auth0/android/util/AuthenticationAPIMockServer.kt +++ b/auth0/src/test/java/com/auth0/android/util/AuthenticationAPIMockServer.kt @@ -1,6 +1,6 @@ package com.auth0.android.util -import okhttp3.mockwebserver.MockResponse +import mockwebserver3.MockResponse import java.nio.file.Files import java.nio.file.Paths @@ -189,15 +189,17 @@ internal class AuthenticationAPIMockServer : APIMockServer() { } private fun responseEmpty(statusCode: Int): MockResponse { - return MockResponse() - .setResponseCode(statusCode) + return MockResponse.Builder() + .code(statusCode) + .build() } private fun responseWithPlainText(statusMessage: String, statusCode: Int): MockResponse { - return MockResponse() - .setResponseCode(statusCode) + return MockResponse.Builder() + .code(statusCode) .addHeader("Content-Type", "text/plain") - .setBody(statusMessage) + .body(statusMessage) + .build() } companion object { diff --git a/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt b/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt index 542a8e4b6..617301299 100644 --- a/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt +++ b/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt @@ -2,7 +2,7 @@ package com.auth0.android.util import com.auth0.android.request.DefaultClient import com.auth0.android.request.internal.GsonProvider -import okhttp3.mockwebserver.MockWebServer +import mockwebserver3.MockWebServer import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import java.net.InetAddress @@ -44,7 +44,7 @@ internal object SSLTestUtils { fun createMockWebServer(): MockWebServer { val mockServer = MockWebServer() - mockServer.useHttps(serverCertificates.sslSocketFactory(), false) + mockServer.useHttps(serverCertificates.sslSocketFactory()) return mockServer } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7f94c35eb..d01397000 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "2.0.21" + ext.kotlin_version = "2.2.0" repositories { google() mavenCentral() From a6434693e9c0d0ebe614fba59cd7f0d8fd15f281 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Thu, 12 Feb 2026 09:35:53 +0530 Subject: [PATCH 10/28] Fix flaky JWKS tests with Mockito.timeout for async callback --- .../java/com/auth0/android/provider/WebAuthProviderTest.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index aa619ecc1..50da1d57f 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -1587,7 +1587,8 @@ public class WebAuthProviderTest { Assert.assertTrue(resume(intent)) mockAPI.takeRequest() ShadowLooper.idleMainLooper() - verify(authCallback).onFailure(authExceptionCaptor.capture()) + // Use Mockito timeout to handle async JWKS response processing on slower CI environments + verify(authCallback, Mockito.timeout(5000)).onFailure(authExceptionCaptor.capture()) val error = authExceptionCaptor.firstValue assertThat(error, `is`(notNullValue())) assertThat( @@ -1720,7 +1721,8 @@ public class WebAuthProviderTest { Assert.assertTrue(resume(intent)) mockAPI.takeRequest() ShadowLooper.idleMainLooper() - verify(authCallback).onFailure(authExceptionCaptor.capture()) + // Use Mockito timeout to handle async JWKS response processing on slower CI environments + verify(authCallback, Mockito.timeout(5000)).onFailure(authExceptionCaptor.capture()) val error = authExceptionCaptor.firstValue assertThat(error, `is`(notNullValue())) assertThat( From f7d74685fd160559778932f7c5db3788a5b42d7e Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Thu, 12 Feb 2026 10:09:01 +0530 Subject: [PATCH 11/28] Fix flaky JWKS tests by mocking NetworkingClient instead of MockWebServer --- .../android/provider/WebAuthProviderTest.kt | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index 50da1d57f..a3efa06ba 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -1543,11 +1543,11 @@ public class WebAuthProviderTest { public fun shouldFailToResumeLoginWhenRSAKeyIsMissingFromJWKSet() { val pkce = Mockito.mock(PKCE::class.java) `when`(pkce.codeChallenge).thenReturn("challenge") - val mockAPI = AuthenticationAPIMockServer() - mockAPI.willReturnEmptyJsonWebKeys() + val networkingClient: NetworkingClient = Mockito.spy(DefaultClient()) val authCallback = mock>() - val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) - proxyAccount.networkingClient = SSLTestUtils.testClient + val proxyAccount = + Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN) + proxyAccount.networkingClient = networkingClient login(proxyAccount) .withState("1234567890") .withNonce(JwtTestUtils.EXPECTED_NONCE) @@ -1584,11 +1584,17 @@ public class WebAuthProviderTest { callbackCaptor.firstValue.onSuccess(codeCredentials) null }.`when`(pkce).getToken(eq("1234"), callbackCaptor.capture()) + // Mock JWKS response with empty keys (no matching RSA key for kid) + val emptyJwksJson = """{"keys": []}""" + val jwksInputStream: InputStream = ByteArrayInputStream(emptyJwksJson.toByteArray()) + val jwksResponse = ServerResponse(200, jwksInputStream, emptyMap()) + Mockito.doReturn(jwksResponse).`when`(networkingClient).load( + eq(proxyAccount.getDomainUrl() + ".well-known/jwks.json"), + any() + ) Assert.assertTrue(resume(intent)) - mockAPI.takeRequest() ShadowLooper.idleMainLooper() - // Use Mockito timeout to handle async JWKS response processing on slower CI environments - verify(authCallback, Mockito.timeout(5000)).onFailure(authExceptionCaptor.capture()) + verify(authCallback).onFailure(authExceptionCaptor.capture()) val error = authExceptionCaptor.firstValue assertThat(error, `is`(notNullValue())) assertThat( @@ -1602,7 +1608,6 @@ public class WebAuthProviderTest { error.cause?.message, `is`("Could not find a public key for kid \"key123\"") ) - mockAPI.shutdown() } @Test @@ -1678,11 +1683,11 @@ public class WebAuthProviderTest { public fun shouldFailToResumeLoginWhenKeyIdIsMissingFromIdTokenHeader() { val pkce = Mockito.mock(PKCE::class.java) `when`(pkce.codeChallenge).thenReturn("challenge") - val mockAPI = AuthenticationAPIMockServer() - mockAPI.willReturnValidJsonWebKeys() + val networkingClient: NetworkingClient = Mockito.spy(DefaultClient()) val authCallback = mock>() - val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) - proxyAccount.networkingClient = SSLTestUtils.testClient + val proxyAccount = + Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN) + proxyAccount.networkingClient = networkingClient login(proxyAccount) .withState("1234567890") .withNonce("abcdefg") @@ -1718,11 +1723,17 @@ public class WebAuthProviderTest { callbackCaptor.firstValue.onSuccess(codeCredentials) null }.`when`(pkce).getToken(eq("1234"), callbackCaptor.capture()) + // Mock JWKS response with valid keys + val encoded = Files.readAllBytes(Paths.get("src/test/resources/rsa_jwks.json")) + val jwksInputStream: InputStream = ByteArrayInputStream(encoded) + val jwksResponse = ServerResponse(200, jwksInputStream, emptyMap()) + Mockito.doReturn(jwksResponse).`when`(networkingClient).load( + eq(proxyAccount.getDomainUrl() + ".well-known/jwks.json"), + any() + ) Assert.assertTrue(resume(intent)) - mockAPI.takeRequest() ShadowLooper.idleMainLooper() - // Use Mockito timeout to handle async JWKS response processing on slower CI environments - verify(authCallback, Mockito.timeout(5000)).onFailure(authExceptionCaptor.capture()) + verify(authCallback).onFailure(authExceptionCaptor.capture()) val error = authExceptionCaptor.firstValue assertThat(error, `is`(notNullValue())) assertThat( @@ -1736,7 +1747,6 @@ public class WebAuthProviderTest { error.cause?.message, `is`("Could not find a public key for kid \"null\"") ) - mockAPI.shutdown() } @Test From c8161b3066880499b2a561cf35daa3ace9c904fb Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Fri, 13 Feb 2026 13:36:46 +0530 Subject: [PATCH 12/28] Fix flaky JWKS tests by shadowing mainThread in ThreadSwitcherShadow --- .../auth0/android/request/internal/ThreadSwitcherShadow.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java b/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java index 0c096ba3f..17100bf9f 100644 --- a/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java +++ b/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java @@ -26,4 +26,9 @@ public ThreadSwitcherShadow() { public void backgroundThread(Runnable runnable) { executor.execute(runnable); } + + @Implementation + public void mainThread(Runnable runnable) { + executor.execute(runnable); + } } From 2eae8a385eb8be4be21036de73235176d540156a Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Fri, 13 Feb 2026 14:34:16 +0530 Subject: [PATCH 13/28] Robolectric 4.14+ uses Conscrypt as the crypto provider on Linux, which causes NullPointerException at OpenSSLKey.java:242 when handling RSA keys created from PEM/PKCS8 specs. --- .../java/com/auth0/android/provider/WebAuthProviderTest.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index a3efa06ba..ea0976730 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -56,6 +56,7 @@ import org.mockito.MockitoAnnotations import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config +import org.robolectric.annotation.ConscryptMode import org.robolectric.shadows.ShadowLooper import java.io.ByteArrayInputStream import java.io.InputStream @@ -1537,7 +1538,7 @@ public class WebAuthProviderTest { } - // TODO: https://auth0team.atlassian.net/browse/SDK-7752 + @ConscryptMode(ConscryptMode.Mode.OFF) @Test @Throws(Exception::class) public fun shouldFailToResumeLoginWhenRSAKeyIsMissingFromJWKSet() { @@ -1677,7 +1678,7 @@ public class WebAuthProviderTest { } - //TODO: https://auth0team.atlassian.net/browse/SDK-7752 + @ConscryptMode(ConscryptMode.Mode.OFF) @Test @Throws(Exception::class) public fun shouldFailToResumeLoginWhenKeyIdIsMissingFromIdTokenHeader() { From 1047611f2ac6157a2cf33a42be42a0734f9ce0a6 Mon Sep 17 00:00:00 2001 From: Prince Mathew <17837162+pmathew92@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:46:55 +0530 Subject: [PATCH 14/28] Revert "Upgrade OkHttp to 5.0.0 and Kotlin to 2.2.0 and removed @ignore testcases." --- V4_MIGRATION_GUIDE.md | 17 +- auth0/build.gradle | 4 +- .../auth0/android/provider/WebAuthProvider.kt | 4 +- .../android/request/internal/OidcUtils.kt | 2 +- .../AuthenticationAPIClientTest.kt | 496 +++++++++--------- .../authentication/MfaApiClientTest.kt | 45 +- .../android/management/UsersAPIClientTest.kt | 52 +- .../myaccount/MyAccountAPIClientTest.kt | 50 +- .../android/provider/WebAuthProviderTest.kt | 44 +- .../android/request/DefaultClientTest.kt | 21 +- .../internal/ThreadSwitcherShadow.java | 5 - .../com/auth0/android/util/APIMockServer.kt | 25 +- .../util/AuthenticationAPIMockServer.kt | 14 +- .../com/auth0/android/util/SSLTestUtils.kt | 4 +- build.gradle | 2 +- 15 files changed, 376 insertions(+), 409 deletions(-) diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md index 5b0e083bc..b8ca744b3 100644 --- a/V4_MIGRATION_GUIDE.md +++ b/V4_MIGRATION_GUIDE.md @@ -50,16 +50,14 @@ buildscript { ### Kotlin Version -v4 uses **Kotlin 2.2.0**. If you're using Kotlin in your project, you may need to update your Kotlin version to ensure compatibility. +v4 uses **Kotlin 2.0.21**. If you're using Kotlin in your project, you may need to update your Kotlin version to ensure compatibility. ```groovy buildscript { - ext.kotlin_version = "2.2.0" + ext.kotlin_version = "2.0.21" } ``` -> **Note:** Kotlin 2.2.0 promotes the deprecation of `String.toLowerCase()` / `String.toUpperCase()` without a `Locale` parameter to an error. If your project uses these methods, replace them with `lowercase(Locale.ROOT)` / `uppercase(Locale.ROOT)`. - ## Breaking Changes ### Classes Removed @@ -98,17 +96,6 @@ implementation 'com.google.code.gson:gson:2.8.9' // your preferred version > **Note:** Pinning or excluding is not recommended long-term, as the SDK has been tested and validated against Gson 2.11.0. -### OkHttp 4.12.0 → 5.0.0 (Internal Dependency) - -v4 upgrades the internal OkHttp dependency from **4.12.0** to **5.0.0**. OkHttp is used as an `implementation` dependency and is **not** exposed in the SDK's public API, so this change should be transparent to most applications. - -However, if your app provides a custom `NetworkingClient` implementation that interacts with OkHttp types, or if you depend on OkHttp transitively through the SDK, be aware of the following: - -- **OkHttp 5.0.0 requires Kotlin 2.2.0+** at compile time. This is already satisfied by the SDK's Kotlin version requirement. -- **Okio 3.x is now required.** OkHttp 5.0.0 depends on Okio 3.x (previously Okio 2.x). If your app uses Okio directly, you may need to update your Okio dependency. - -> **Note:** Since OkHttp is an internal dependency of the SDK, no changes are required in your application code unless you are directly depending on OkHttp types from this SDK's classpath. - ## Getting Help If you encounter issues during migration: diff --git a/auth0/build.gradle b/auth0/build.gradle index d4769bfce..28ff39fb9 100644 --- a/auth0/build.gradle +++ b/auth0/build.gradle @@ -80,7 +80,7 @@ android { } ext { - okhttpVersion = '5.0.0' + okhttpVersion = '4.12.0' coroutinesVersion = '1.10.2' biometricLibraryVersion = '1.1.0' } @@ -104,7 +104,7 @@ dependencies { testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0' testImplementation 'org.mockito:mockito-core:5.14.0' testImplementation 'org.mockito.kotlin:mockito-kotlin:5.4.0' - testImplementation "com.squareup.okhttp3:mockwebserver3:$okhttpVersion" + testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" testImplementation "com.squareup.okhttp3:okhttp-tls:$okhttpVersion" testImplementation 'com.jayway.awaitility:awaitility:1.7.0' testImplementation 'org.robolectric:robolectric:4.15.1' diff --git a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt index 09920438a..955eec9c5 100644 --- a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt +++ b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt @@ -176,7 +176,7 @@ public object WebAuthProvider : SenderConstraining { * @return the current builder instance */ public fun withScheme(scheme: String): LogoutBuilder { - val lowerCase = scheme.lowercase(Locale.ROOT) + val lowerCase = scheme.toLowerCase(Locale.ROOT) if (scheme != lowerCase) { Log.w( TAG, @@ -397,7 +397,7 @@ public object WebAuthProvider : SenderConstraining { * @return the current builder instance */ public fun withScheme(scheme: String): Builder { - val lowerCase = scheme.lowercase(Locale.ROOT) + val lowerCase = scheme.toLowerCase(Locale.ROOT) if (scheme != lowerCase) { Log.w( TAG, diff --git a/auth0/src/main/java/com/auth0/android/request/internal/OidcUtils.kt b/auth0/src/main/java/com/auth0/android/request/internal/OidcUtils.kt index ee5d2bc25..bcb22f678 100644 --- a/auth0/src/main/java/com/auth0/android/request/internal/OidcUtils.kt +++ b/auth0/src/main/java/com/auth0/android/request/internal/OidcUtils.kt @@ -19,7 +19,7 @@ internal object OidcUtils { */ fun includeRequiredScope(scope: String): String { val existingScopes = scope.split(" ") - .map { it.lowercase(Locale.ROOT) } + .map { it.toLowerCase(Locale.ROOT) } return if (!existingScopes.contains(REQUIRED_SCOPE)) { (existingScopes + REQUIRED_SCOPE).joinToString(separator = " ").trim() } else { diff --git a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt index 60a28ae9c..350ec5d23 100755 --- a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt @@ -41,7 +41,7 @@ import org.mockito.kotlin.whenever import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import mockwebserver3.RecordedRequest +import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers import org.hamcrest.collection.IsMapContaining @@ -183,12 +183,12 @@ public class AuthenticationAPIClientTest { ) val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( body, @@ -221,12 +221,12 @@ public class AuthenticationAPIClientTest { ) val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( body, @@ -250,12 +250,12 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/passkey/register")) + assertThat(request.path, Matchers.equalTo("/passkey/register")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("realm", MY_CONNECTION)) assertThat(body, Matchers.hasEntry("organization", "testOrganization")) @@ -273,12 +273,12 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/passkey/challenge")) + assertThat(request.path, Matchers.equalTo("/passkey/challenge")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("realm", MY_CONNECTION)) assertThat(body, Matchers.hasEntry("organization", "testOrganization")) @@ -304,12 +304,12 @@ public class AuthenticationAPIClientTest { assertThat(callback.payload.recoveryCode, Matchers.`is`("654321")) val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( body, @@ -336,12 +336,12 @@ public class AuthenticationAPIClientTest { ) val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( body, @@ -369,12 +369,12 @@ public class AuthenticationAPIClientTest { ) val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( body, @@ -395,11 +395,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/mfa/challenge")) + assertThat(request.path, Matchers.equalTo("/mfa/challenge")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("mfa_token", "ey30.the-mfa-token.value")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) @@ -422,11 +422,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/mfa/challenge")) + assertThat(request.path, Matchers.equalTo("/mfa/challenge")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("mfa_token", "ey30.the-mfa-token.value")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) @@ -451,7 +451,7 @@ public class AuthenticationAPIClientTest { assertThat(credentials.user.getId(), Matchers.`is`("auth0|123456")) val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) @@ -475,7 +475,7 @@ public class AuthenticationAPIClientTest { assertThat(credentials, Matchers.`is`(Matchers.notNullValue())) val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) @@ -505,12 +505,12 @@ public class AuthenticationAPIClientTest { ) val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( body, @@ -537,9 +537,9 @@ public class AuthenticationAPIClientTest { ) ) val request = mockAPI.takeRequest() - assertThat(request.target, Matchers.`is`("/oauth/token")) + assertThat(request.path, Matchers.`is`("/oauth/token")) assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) @@ -562,9 +562,9 @@ public class AuthenticationAPIClientTest { .execute() assertThat(credentials, Matchers.`is`(Matchers.notNullValue())) val request = mockAPI.takeRequest() - assertThat(request.target, Matchers.`is`("/oauth/token")) + assertThat(request.path, Matchers.`is`("/oauth/token")) assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) @@ -588,9 +588,9 @@ public class AuthenticationAPIClientTest { .await() assertThat(credentials, Matchers.`is`(Matchers.notNullValue())) val request = mockAPI.takeRequest() - assertThat(request.target, Matchers.`is`("/oauth/token")) + assertThat(request.path, Matchers.`is`("/oauth/token")) assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) @@ -619,15 +619,15 @@ public class AuthenticationAPIClientTest { ) val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) assertThat( - request.headers["Authorization"], + request.getHeader("Authorization"), Matchers.`is`("Bearer ACCESS_TOKEN") ) - assertThat(request.target, Matchers.equalTo("/userinfo")) + assertThat(request.path, Matchers.equalTo("/userinfo")) } @Test @@ -639,15 +639,15 @@ public class AuthenticationAPIClientTest { assertThat(profile, Matchers.`is`(Matchers.notNullValue())) val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) assertThat( - request.headers["Authorization"], + request.getHeader("Authorization"), Matchers.`is`("Bearer ACCESS_TOKEN") ) - assertThat(request.target, Matchers.equalTo("/userinfo")) + assertThat(request.path, Matchers.equalTo("/userinfo")) } @Test @@ -660,15 +660,15 @@ public class AuthenticationAPIClientTest { assertThat(profile, Matchers.`is`(Matchers.notNullValue())) val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) assertThat( - request.headers["Authorization"], + request.getHeader("Authorization"), Matchers.`is`("Bearer ACCESS_TOKEN") ) - assertThat(request.target, Matchers.equalTo("/userinfo")) + assertThat(request.path, Matchers.equalTo("/userinfo")) } @Test @@ -680,11 +680,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -709,11 +709,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -735,11 +735,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -763,11 +763,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -795,11 +795,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -826,11 +826,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -854,11 +854,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -882,11 +882,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -914,11 +914,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -946,11 +946,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -975,11 +975,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -1002,11 +1002,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1029,11 +1029,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1053,11 +1053,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1076,11 +1076,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1101,11 +1101,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1123,11 +1123,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1145,11 +1145,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1170,11 +1170,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1192,11 +1192,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1214,11 +1214,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1238,11 +1238,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1259,11 +1259,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1284,11 +1284,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1300,7 +1300,7 @@ public class AuthenticationAPIClientTest { ) ) val loginRequest = mockAPI.takeRequest() - assertThat(loginRequest.target, Matchers.equalTo("/oauth/token")) + assertThat(loginRequest.path, Matchers.equalTo("/oauth/token")) val loginBody = bodyFromRequest(loginRequest) assertThat(loginBody, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(loginBody, Matchers.hasEntry("password", PASSWORD)) @@ -1325,11 +1325,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1356,11 +1356,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1369,7 +1369,7 @@ public class AuthenticationAPIClientTest { assertThat(body, Matchers.hasEntry("user_metadata", testMetadata)) assertThat(credentials, Matchers.`is`(Matchers.notNullValue())) val loginRequest = mockAPI.takeRequest() - assertThat(loginRequest.target, Matchers.equalTo("/oauth/token")) + assertThat(loginRequest.path, Matchers.equalTo("/oauth/token")) val loginBody = bodyFromRequest(loginRequest) assertThat(loginBody, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(loginBody, Matchers.hasEntry("password", PASSWORD)) @@ -1392,11 +1392,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1404,7 +1404,7 @@ public class AuthenticationAPIClientTest { assertThat(body, Matchers.hasEntry("connection", MY_CONNECTION)) assertThat(credentials, Matchers.`is`(Matchers.notNullValue())) val loginRequest = mockAPI.takeRequest() - assertThat(loginRequest.target, Matchers.equalTo("/oauth/token")) + assertThat(loginRequest.path, Matchers.equalTo("/oauth/token")) val loginBody = bodyFromRequest(loginRequest) assertThat(loginBody, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(loginBody, Matchers.hasEntry("password", PASSWORD)) @@ -1429,11 +1429,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("username", SUPPORT)) @@ -1441,7 +1441,7 @@ public class AuthenticationAPIClientTest { assertThat(body, Matchers.hasEntry("connection", MY_CONNECTION)) assertThat(credentials, Matchers.`is`(Matchers.notNullValue())) val loginRequest = mockAPI.takeRequest() - assertThat(loginRequest.target, Matchers.equalTo("/oauth/token")) + assertThat(loginRequest.path, Matchers.equalTo("/oauth/token")) val loginBody = bodyFromRequest(loginRequest) assertThat(loginBody, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(loginBody, Matchers.hasEntry("password", PASSWORD)) @@ -1465,11 +1465,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("password", PASSWORD)) @@ -1480,7 +1480,7 @@ public class AuthenticationAPIClientTest { ) ) val loginRequest = mockAPI.takeRequest() - assertThat(loginRequest.target, Matchers.equalTo("/oauth/token")) + assertThat(loginRequest.path, Matchers.equalTo("/oauth/token")) val loginBody = bodyFromRequest(loginRequest) assertThat(loginBody, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(loginBody, Matchers.hasEntry("password", PASSWORD)) @@ -1506,11 +1506,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1522,7 +1522,7 @@ public class AuthenticationAPIClientTest { ) ) val loginRequest = mockAPI.takeRequest() - assertThat(loginRequest.target, Matchers.equalTo("/oauth/token")) + assertThat(loginRequest.path, Matchers.equalTo("/oauth/token")) val loginBody = bodyFromRequest(loginRequest) assertThat(loginBody, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(loginBody, Matchers.hasEntry("password", PASSWORD)) @@ -1540,11 +1540,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/change_password")) + assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1559,11 +1559,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/change_password")) + assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1578,11 +1578,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/change_password")) + assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1598,11 +1598,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/change_password")) + assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1618,11 +1618,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/change_password")) + assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1638,11 +1638,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/dbconnections/change_password")) + assertThat(request.path, Matchers.equalTo("/dbconnections/change_password")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.not(Matchers.hasKey("username"))) @@ -1659,11 +1659,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1681,11 +1681,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1701,11 +1701,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1721,11 +1721,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1742,11 +1742,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1764,11 +1764,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1784,11 +1784,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1804,11 +1804,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1829,11 +1829,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1851,11 +1851,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1871,11 +1871,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1891,11 +1891,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("email", SUPPORT_AUTH0_COM)) @@ -1912,11 +1912,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -1934,11 +1934,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -1954,11 +1954,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -1974,11 +1974,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -1995,11 +1995,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2017,11 +2017,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2037,11 +2037,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2057,11 +2057,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2078,11 +2078,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2100,11 +2100,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2120,11 +2120,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2140,11 +2140,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("phone_number", "+1123123123")) @@ -2160,9 +2160,9 @@ public class AuthenticationAPIClientTest { .start(callback) ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.target, Matchers.equalTo("/.well-known/jwks.json")) + assertThat(request.path, Matchers.equalTo("/.well-known/jwks.json")) assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) @@ -2175,9 +2175,9 @@ public class AuthenticationAPIClientTest { val result = client.fetchJsonWebKeys() .execute() val request = mockAPI.takeRequest() - assertThat(request.target, Matchers.equalTo("/.well-known/jwks.json")) + assertThat(request.path, Matchers.equalTo("/.well-known/jwks.json")) assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) @@ -2192,9 +2192,9 @@ public class AuthenticationAPIClientTest { val result = client.fetchJsonWebKeys() .await() val request = mockAPI.takeRequest() - assertThat(request.target, Matchers.equalTo("/.well-known/jwks.json")) + assertThat(request.path, Matchers.equalTo("/.well-known/jwks.json")) assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) @@ -2211,7 +2211,7 @@ public class AuthenticationAPIClientTest { .start(callback) ShadowLooper.idleMainLooper() val firstRequest = mockAPI.takeRequest() - assertThat(firstRequest.target, Matchers.equalTo("/oauth/token")) + assertThat(firstRequest.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(firstRequest) assertThat(body, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("password", "voidpassword")) @@ -2223,10 +2223,10 @@ public class AuthenticationAPIClientTest { ) val secondRequest = mockAPI.takeRequest() assertThat( - secondRequest.headers["Authorization"], + secondRequest.getHeader("Authorization"), Matchers.`is`("Bearer " + AuthenticationAPIMockServer.ACCESS_TOKEN) ) - assertThat(secondRequest.target, Matchers.equalTo("/userinfo")) + assertThat(secondRequest.path, Matchers.equalTo("/userinfo")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( Authentication::class.java @@ -2245,11 +2245,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/revoke")) + assertThat(request.path, Matchers.equalTo("/oauth/revoke")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("token", "refreshToken")) @@ -2265,11 +2265,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/revoke")) + assertThat(request.path, Matchers.equalTo("/oauth/revoke")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("token", "refreshToken")) @@ -2285,11 +2285,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/revoke")) + assertThat(request.path, Matchers.equalTo("/oauth/revoke")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("token", "refreshToken")) @@ -2304,11 +2304,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -2334,11 +2334,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -2361,11 +2361,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -2388,11 +2388,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -2418,11 +2418,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -2443,11 +2443,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat( @@ -2473,11 +2473,11 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.not(Matchers.hasKey("scope"))) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) @@ -2499,11 +2499,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("refresh_token", "refreshToken")) @@ -2522,11 +2522,11 @@ public class AuthenticationAPIClientTest { .await() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("refresh_token", "refreshToken")) @@ -2545,11 +2545,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("refresh_token", "refreshToken")) @@ -2567,11 +2567,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("refresh_token", "refreshToken")) @@ -2591,11 +2591,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("refresh_token", "refreshToken")) @@ -2614,11 +2614,11 @@ public class AuthenticationAPIClientTest { .execute() val request = mockAPI.takeRequest() assertThat( - request.headers["Accept-Language"], Matchers.`is`( + request.getHeader("Accept-Language"), Matchers.`is`( defaultLocale ) ) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("client_id", CLIENT_ID)) assertThat(body, Matchers.hasEntry("refresh_token", "refreshToken")) @@ -2641,7 +2641,7 @@ public class AuthenticationAPIClientTest { ) .execute() val firstRequest = mockAPI.takeRequest() - assertThat(firstRequest.target, Matchers.equalTo("/oauth/token")) + assertThat(firstRequest.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(firstRequest) assertThat(body, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("password", "voidpassword")) @@ -2653,10 +2653,10 @@ public class AuthenticationAPIClientTest { ) val secondRequest = mockAPI.takeRequest() assertThat( - secondRequest.headers["Authorization"], + secondRequest.getHeader("Authorization"), Matchers.`is`("Bearer " + AuthenticationAPIMockServer.ACCESS_TOKEN) ) - assertThat(secondRequest.target, Matchers.equalTo("/userinfo")) + assertThat(secondRequest.path, Matchers.equalTo("/userinfo")) assertThat(authentication, Matchers.`is`(Matchers.notNullValue())) } @@ -2674,7 +2674,7 @@ public class AuthenticationAPIClientTest { ) .await() val firstRequest = mockAPI.takeRequest() - assertThat(firstRequest.target, Matchers.equalTo("/oauth/token")) + assertThat(firstRequest.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(firstRequest) assertThat(body, Matchers.hasEntry("username", SUPPORT_AUTH0_COM)) assertThat(body, Matchers.hasEntry("password", "voidpassword")) @@ -2686,10 +2686,10 @@ public class AuthenticationAPIClientTest { ) val secondRequest = mockAPI.takeRequest() assertThat( - secondRequest.headers["Authorization"], + secondRequest.getHeader("Authorization"), Matchers.`is`("Bearer " + AuthenticationAPIMockServer.ACCESS_TOKEN) ) - assertThat(secondRequest.target, Matchers.equalTo("/userinfo")) + assertThat(secondRequest.path, Matchers.equalTo("/userinfo")) assertThat(authentication, Matchers.`is`(Matchers.notNullValue())) } @@ -2702,7 +2702,7 @@ public class AuthenticationAPIClientTest { .start(callback) ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -2727,7 +2727,7 @@ public class AuthenticationAPIClientTest { .start(callback) ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -2759,8 +2759,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.nullValue()) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.getHeader("DPoP"), Matchers.nullValue()) + assertThat(request.path, Matchers.equalTo("/oauth/token")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( Credentials::class.java @@ -2776,8 +2776,8 @@ public class AuthenticationAPIClientTest { val challengeResponse = client.passkeyChallenge(MY_CONNECTION, "testOrganization") .execute() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.nullValue()) - assertThat(request.target, Matchers.equalTo("/passkey/challenge")) + assertThat(request.getHeader("DPoP"), Matchers.nullValue()) + assertThat(request.path, Matchers.equalTo("/passkey/challenge")) assertThat(challengeResponse, Matchers.`is`(Matchers.notNullValue())) } @@ -2790,8 +2790,8 @@ public class AuthenticationAPIClientTest { val challengeResponse = client.passkeyChallenge(MY_CONNECTION, "testOrganization") .execute() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.nullValue()) - assertThat(request.target, Matchers.equalTo("/passkey/challenge")) + assertThat(request.getHeader("DPoP"), Matchers.nullValue()) + assertThat(request.path, Matchers.equalTo("/passkey/challenge")) assertThat(challengeResponse, Matchers.`is`(Matchers.notNullValue())) } @@ -2808,8 +2808,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.notNullValue()) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.getHeader("DPoP"), Matchers.notNullValue()) + assertThat(request.path, Matchers.equalTo("/oauth/token")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( Credentials::class.java @@ -2829,8 +2829,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.nullValue()) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.getHeader("DPoP"), Matchers.nullValue()) + assertThat(request.path, Matchers.equalTo("/oauth/token")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( Credentials::class.java @@ -2848,8 +2848,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.nullValue()) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.getHeader("DPoP"), Matchers.nullValue()) + assertThat(request.path, Matchers.equalTo("/oauth/token")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( Credentials::class.java @@ -2871,8 +2871,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.notNullValue()) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.getHeader("DPoP"), Matchers.notNullValue()) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( @@ -2906,8 +2906,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.nullValue()) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.getHeader("DPoP"), Matchers.nullValue()) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat(body, Matchers.hasEntry("grant_type", "refresh_token")) @@ -2938,8 +2938,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.notNullValue()) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.getHeader("DPoP"), Matchers.notNullValue()) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( @@ -2973,9 +2973,9 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.notNullValue()) - assertThat(request.headers["Authorization"], Matchers.`is`("DPoP ACCESS_TOKEN")) - assertThat(request.target, Matchers.equalTo("/userinfo")) + assertThat(request.getHeader("DPoP"), Matchers.notNullValue()) + assertThat(request.getHeader("Authorization"), Matchers.`is`("DPoP ACCESS_TOKEN")) + assertThat(request.path, Matchers.equalTo("/userinfo")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( UserProfile::class.java @@ -2993,9 +2993,9 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.nullValue()) - assertThat(request.headers["Authorization"], Matchers.`is`("Bearer ACCESS_TOKEN")) - assertThat(request.target, Matchers.equalTo("/userinfo")) + assertThat(request.getHeader("DPoP"), Matchers.nullValue()) + assertThat(request.getHeader("Authorization"), Matchers.`is`("Bearer ACCESS_TOKEN")) + assertThat(request.path, Matchers.equalTo("/userinfo")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( UserProfile::class.java @@ -3017,8 +3017,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.nullValue()) - assertThat(request.target, Matchers.equalTo("/dbconnections/signup")) + assertThat(request.getHeader("DPoP"), Matchers.nullValue()) + assertThat(request.path, Matchers.equalTo("/dbconnections/signup")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( DatabaseUser::class.java @@ -3040,8 +3040,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.nullValue()) - assertThat(request.target, Matchers.equalTo("/passwordless/start")) + assertThat(request.getHeader("DPoP"), Matchers.nullValue()) + assertThat(request.path, Matchers.equalTo("/passwordless/start")) assertThat(callback, AuthenticationCallbackMatcher.hasNoError()) } @@ -3059,8 +3059,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.nullValue()) - assertThat(request.target, Matchers.equalTo("/.well-known/jwks.json")) + assertThat(request.getHeader("DPoP"), Matchers.nullValue()) + assertThat(request.path, Matchers.equalTo("/.well-known/jwks.json")) assertThat(callback, AuthenticationCallbackMatcher.hasPayload(emptyMap())) } @@ -3077,8 +3077,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.notNullValue()) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.getHeader("DPoP"), Matchers.notNullValue()) + assertThat(request.path, Matchers.equalTo("/oauth/token")) val body = bodyFromRequest(request) assertThat( body, @@ -3104,8 +3104,8 @@ public class AuthenticationAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.headers["DPoP"], Matchers.notNullValue()) - assertThat(request.target, Matchers.equalTo("/oauth/token")) + assertThat(request.getHeader("DPoP"), Matchers.notNullValue()) + assertThat(request.path, Matchers.equalTo("/oauth/token")) assertThat( callback, AuthenticationCallbackMatcher.hasPayloadOfType( SSOCredentials::class.java @@ -3134,7 +3134,7 @@ public class AuthenticationAPIClientTest { private inline fun bodyFromRequest(request: RecordedRequest): Map { val mapType = object : TypeToken>() {}.type - return gson.fromJson(request.body!!.utf8(), mapType) + return gson.fromJson(request.body.readUtf8(), mapType) } private val defaultLocale: String diff --git a/auth0/src/test/java/com/auth0/android/authentication/MfaApiClientTest.kt b/auth0/src/test/java/com/auth0/android/authentication/MfaApiClientTest.kt index 8090187bd..8fec80b5f 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/MfaApiClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/MfaApiClientTest.kt @@ -20,9 +20,9 @@ import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import mockwebserver3.MockResponse -import mockwebserver3.MockWebServer -import mockwebserver3.RecordedRequest +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.* import org.junit.After @@ -57,16 +57,15 @@ public class MfaApiClientTest { @After public fun tearDown(): Unit { - mockServer.close() + mockServer.shutdown() } private fun enqueueMockResponse(json: String, statusCode: Int = 200): Unit { mockServer.enqueue( - MockResponse.Builder() - .code(statusCode) + MockResponse() + .setResponseCode(statusCode) .addHeader("Content-Type", "application/json") - .body(json) - .build() + .setBody(json) ) } @@ -77,7 +76,7 @@ public class MfaApiClientTest { private inline fun bodyFromRequest(request: RecordedRequest): Map { val mapType = object : TypeToken>() {}.type - return gson.fromJson(request.body!!.utf8(), mapType) + return gson.fromJson(request.body.readUtf8(), mapType) } @@ -140,8 +139,8 @@ public class MfaApiClientTest { mfaClient.getAuthenticators(listOf("oob")).await() val request = mockServer.takeRequest() - assertThat(request.headers["Authorization"], `is`("Bearer $MFA_TOKEN")) - assertThat(request.target, `is`("/mfa/authenticators")) + assertThat(request.getHeader("Authorization"), `is`("Bearer $MFA_TOKEN")) + assertThat(request.path, `is`("/mfa/authenticators")) assertThat(request.method, `is`("GET")) } @@ -194,9 +193,9 @@ public class MfaApiClientTest { mfaClient.enroll(MfaEnrollmentType.Phone("+12025550135")).await() val request = mockServer.takeRequest() - assertThat(request.target, `is`("/mfa/associate")) + assertThat(request.path, `is`("/mfa/associate")) assertThat(request.method, `is`("POST")) - assertThat(request.headers["Authorization"], `is`("Bearer $MFA_TOKEN")) + assertThat(request.getHeader("Authorization"), `is`("Bearer $MFA_TOKEN")) val body = bodyFromRequest(request) assertThat(body["authenticator_types"], `is`(listOf("oob"))) @@ -241,9 +240,9 @@ public class MfaApiClientTest { mfaClient.enroll(MfaEnrollmentType.Email("user@example.com")).await() val request = mockServer.takeRequest() - assertThat(request.target, `is`("/mfa/associate")) + assertThat(request.path, `is`("/mfa/associate")) assertThat(request.method, `is`("POST")) - assertThat(request.headers["Authorization"], `is`("Bearer $MFA_TOKEN")) + assertThat(request.getHeader("Authorization"), `is`("Bearer $MFA_TOKEN")) val body = bodyFromRequest(request) assertThat(body["authenticator_types"], `is`(listOf("oob"))) @@ -298,9 +297,9 @@ public class MfaApiClientTest { mfaClient.enroll(MfaEnrollmentType.Otp).await() val request = mockServer.takeRequest() - assertThat(request.target, `is`("/mfa/associate")) + assertThat(request.path, `is`("/mfa/associate")) assertThat(request.method, `is`("POST")) - assertThat(request.headers["Authorization"], `is`("Bearer $MFA_TOKEN")) + assertThat(request.getHeader("Authorization"), `is`("Bearer $MFA_TOKEN")) val body = bodyFromRequest(request) assertThat(body["authenticator_types"], `is`(listOf("otp"))) @@ -343,9 +342,9 @@ public class MfaApiClientTest { mfaClient.enroll(MfaEnrollmentType.Push).await() val request = mockServer.takeRequest() - assertThat(request.target, `is`("/mfa/associate")) + assertThat(request.path, `is`("/mfa/associate")) assertThat(request.method, `is`("POST")) - assertThat(request.headers["Authorization"], `is`("Bearer $MFA_TOKEN")) + assertThat(request.getHeader("Authorization"), `is`("Bearer $MFA_TOKEN")) val body = bodyFromRequest(request) assertThat(body["authenticator_types"], `is`(listOf("oob"))) @@ -391,7 +390,7 @@ public class MfaApiClientTest { mfaClient.challenge("sms|dev_123").await() val request = mockServer.takeRequest() - assertThat(request.target, `is`("/mfa/challenge")) + assertThat(request.path, `is`("/mfa/challenge")) assertThat(request.method, `is`("POST")) val body = bodyFromRequest(request) @@ -443,7 +442,7 @@ public class MfaApiClientTest { mfaClient.verify(MfaVerificationType.Otp("123456")).await() val request = mockServer.takeRequest() - assertThat(request.target, `is`("/oauth/token")) + assertThat(request.path, `is`("/oauth/token")) assertThat(request.method, `is`("POST")) val body = bodyFromRequest(request) @@ -518,7 +517,7 @@ public class MfaApiClientTest { mfaClient.verify(MfaVerificationType.Oob(oobCode = "oob_code_123", bindingCode = "654321")).await() val request = mockServer.takeRequest() - assertThat(request.target, `is`("/oauth/token")) + assertThat(request.path, `is`("/oauth/token")) assertThat(request.method, `is`("POST")) val body = bodyFromRequest(request) @@ -581,7 +580,7 @@ public class MfaApiClientTest { mfaClient.verify(MfaVerificationType.RecoveryCode("RECOVERY_123")).await() val request = mockServer.takeRequest() - assertThat(request.target, `is`("/oauth/token")) + assertThat(request.path, `is`("/oauth/token")) assertThat(request.method, `is`("POST")) val body = bodyFromRequest(request) diff --git a/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt index 06a11d4ee..7c1d370e9 100755 --- a/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/management/UsersAPIClientTest.kt @@ -21,7 +21,7 @@ import com.google.gson.reflect.TypeToken import org.mockito.kotlin.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import mockwebserver3.RecordedRequest +import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers import org.hamcrest.Matchers.instanceOf @@ -143,11 +143,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.target, + request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities") ) assertThat( - request.headers[HEADER_AUTHORIZATION], + request.getHeader(HEADER_AUTHORIZATION), Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_POST)) @@ -168,11 +168,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.target, + request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities") ) assertThat( - request.headers[HEADER_AUTHORIZATION], + request.getHeader(HEADER_AUTHORIZATION), Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_POST)) @@ -194,11 +194,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.target, + request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities") ) assertThat( - request.headers[HEADER_AUTHORIZATION], + request.getHeader(HEADER_AUTHORIZATION), Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_POST)) @@ -222,11 +222,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.target, + request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities/$PROVIDER/$USER_ID_SECONDARY") ) assertThat( - request.headers[HEADER_AUTHORIZATION], + request.getHeader(HEADER_AUTHORIZATION), Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_DELETE)) @@ -247,11 +247,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.target, + request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities/$PROVIDER/$USER_ID_SECONDARY") ) assertThat( - request.headers[HEADER_AUTHORIZATION], + request.getHeader(HEADER_AUTHORIZATION), Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_DELETE)) @@ -273,11 +273,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.target, + request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY/identities/$PROVIDER/$USER_ID_SECONDARY") ) assertThat( - request.headers[HEADER_AUTHORIZATION], + request.getHeader(HEADER_AUTHORIZATION), Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_DELETE)) @@ -303,9 +303,9 @@ public class UsersAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.target, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY")) + assertThat(request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY")) assertThat( - request.headers[HEADER_AUTHORIZATION], + request.getHeader(HEADER_AUTHORIZATION), Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_PATCH)) @@ -332,11 +332,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.target, + request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY") ) assertThat( - request.headers[HEADER_AUTHORIZATION], + request.getHeader(HEADER_AUTHORIZATION), Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_PATCH)) @@ -361,11 +361,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.target, + request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY") ) assertThat( - request.headers[HEADER_AUTHORIZATION], + request.getHeader(HEADER_AUTHORIZATION), Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat(request.method, Matchers.equalTo(METHOD_PATCH)) @@ -384,11 +384,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.target, + request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY") ) assertThat( - request.headers[HEADER_AUTHORIZATION], + request.getHeader(HEADER_AUTHORIZATION), Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat( @@ -407,11 +407,11 @@ public class UsersAPIClientTest { val request = mockAPI.takeRequest() assertThat( - request.target, + request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY") ) assertThat( - request.headers[HEADER_AUTHORIZATION], + request.getHeader(HEADER_AUTHORIZATION), Matchers.equalTo(BEARER + TOKEN_PRIMARY) ) assertThat( @@ -430,8 +430,8 @@ public class UsersAPIClientTest { ShadowLooper.idleMainLooper() val request = mockAPI.takeRequest() - assertThat(request.target, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY")) - assertThat(request.headers[HEADER_AUTHORIZATION], + assertThat(request.path, Matchers.equalTo("/api/v2/users/$USER_ID_PRIMARY")) + assertThat(request.getHeader(HEADER_AUTHORIZATION), Matchers.equalTo(BEARER + TOKEN_PRIMARY)) assertThat(request.method, Matchers.equalTo(METHOD_GET)) assertThat( @@ -443,7 +443,7 @@ public class UsersAPIClientTest { private inline fun bodyFromRequest(request: RecordedRequest): Map { val mapType = object : TypeToken>() {}.type - return gson.fromJson(request.body!!.utf8(), mapType) + return gson.fromJson(request.body.readUtf8(), mapType) } private companion object { diff --git a/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt b/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt index 499cf1cd4..388530df7 100644 --- a/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/myaccount/MyAccountAPIClientTest.kt @@ -18,7 +18,7 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken import org.mockito.kotlin.mock -import mockwebserver3.RecordedRequest +import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers import org.junit.After @@ -56,7 +56,7 @@ public class MyAccountAPIClientTest { client.passkeyEnrollmentChallenge() .start(callback) val request = mockAPI.takeRequest() - assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods")) + assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods")) } @Test @@ -92,7 +92,7 @@ public class MyAccountAPIClientTest { .start(callback) val request = mockAPI.takeRequest() - val header = request.headers["Authorization"] + val header = request.getHeader("Authorization") assertThat( header, Matchers.`is`( @@ -147,7 +147,7 @@ public class MyAccountAPIClientTest { // Take and verify the request was sent correctly val request = mockAPI.takeRequest() assertThat( - request.target, + request.path, Matchers.equalTo("/me/v1/authentication-methods") ) // Verify error details @@ -175,7 +175,7 @@ public class MyAccountAPIClientTest { } val request = mockAPI.takeRequest() assertThat( - request.target, + request.path, Matchers.equalTo("/me/v1/authentication-methods") ) @@ -206,7 +206,7 @@ public class MyAccountAPIClientTest { .start(callback) val request = mockAPI.takeRequest() assertThat( - request.target, + request.path, Matchers.equalTo("/me/v1/authentication-methods/${AUTHENTICATION_ID}/verify") ) } @@ -248,7 +248,7 @@ public class MyAccountAPIClientTest { .start(callback) val request = mockAPI.takeRequest() - val header = request.headers["Authorization"] + val header = request.getHeader("Authorization") assertThat( header, Matchers.`is`( @@ -299,7 +299,7 @@ public class MyAccountAPIClientTest { // Take and verify the request was sent correctly val request = mockAPI.takeRequest() assertThat( - request.target, + request.path, Matchers.equalTo("/me/v1/authentication-methods/${AUTHENTICATION_ID}/verify") ) assertThat(error, Matchers.notNullValue()) @@ -318,8 +318,8 @@ public class MyAccountAPIClientTest { client.getFactors().start(callback) val request = mockAPI.takeRequest() - assertThat(request.target, Matchers.equalTo("/me/v1/factors")) - assertThat(request.headers["Authorization"], Matchers.equalTo("Bearer $ACCESS_TOKEN")) + assertThat(request.path, Matchers.equalTo("/me/v1/factors")) + assertThat(request.getHeader("Authorization"), Matchers.equalTo("Bearer $ACCESS_TOKEN")) assertThat(request.method, Matchers.equalTo("GET")) } @@ -329,8 +329,8 @@ public class MyAccountAPIClientTest { client.getAuthenticationMethods().start(callback) val request = mockAPI.takeRequest() - assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods")) - assertThat(request.headers["Authorization"], Matchers.equalTo("Bearer $ACCESS_TOKEN")) + assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods")) + assertThat(request.getHeader("Authorization"), Matchers.equalTo("Bearer $ACCESS_TOKEN")) assertThat(request.method, Matchers.equalTo("GET")) } @@ -341,8 +341,8 @@ public class MyAccountAPIClientTest { client.getAuthenticationMethodById(methodId).start(callback) val request = mockAPI.takeRequest() - assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods/email%7C12345")) - assertThat(request.headers["Authorization"], Matchers.equalTo("Bearer $ACCESS_TOKEN")) + assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods/email%7C12345")) + assertThat(request.getHeader("Authorization"), Matchers.equalTo("Bearer $ACCESS_TOKEN")) assertThat(request.method, Matchers.equalTo("GET")) } @@ -353,8 +353,8 @@ public class MyAccountAPIClientTest { client.deleteAuthenticationMethod(methodId).start(callback) val request = mockAPI.takeRequest() - assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods/email%7C12345")) - assertThat(request.headers["Authorization"], Matchers.equalTo("Bearer $ACCESS_TOKEN")) + assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods/email%7C12345")) + assertThat(request.getHeader("Authorization"), Matchers.equalTo("Bearer $ACCESS_TOKEN")) assertThat(request.method, Matchers.equalTo("DELETE")) } @@ -366,7 +366,7 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods/phone%7C12345")) + assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods/phone%7C12345")) assertThat(request.method, Matchers.equalTo("PATCH")) assertThat(body, Matchers.hasEntry("preferred_authentication_method", "sms" as Any)) } @@ -380,7 +380,7 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods/totp%7C12345")) + assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods/totp%7C12345")) assertThat(request.method, Matchers.equalTo("PATCH")) assertThat(body, Matchers.hasEntry("name", name as Any)) } @@ -393,7 +393,7 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods")) + assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods")) assertThat(request.method, Matchers.equalTo("POST")) assertThat(body, Matchers.hasEntry("type", "email" as Any)) assertThat(body, Matchers.hasEntry("email", email as Any)) @@ -407,7 +407,7 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods")) + assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods")) assertThat(request.method, Matchers.equalTo("POST")) assertThat(body, Matchers.hasEntry("type", "phone" as Any)) assertThat(body, Matchers.hasEntry("phone_number", phoneNumber as Any)) @@ -421,7 +421,7 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods")) + assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods")) assertThat(request.method, Matchers.equalTo("POST")) assertThat(body, Matchers.hasEntry("type", "totp" as Any)) } @@ -434,7 +434,7 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods")) + assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods")) assertThat(request.method, Matchers.equalTo("POST")) assertThat(body, Matchers.hasEntry("type", "recovery-code" as Any)) } @@ -449,7 +449,7 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods/email%7C123/verify")) + assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods/email%7C123/verify")) assertThat(request.method, Matchers.equalTo("POST")) assertThat(body, Matchers.hasEntry("otp_code", otp as Any)) assertThat(body, Matchers.hasEntry("auth_session", session as Any)) @@ -462,14 +462,14 @@ public class MyAccountAPIClientTest { val request = mockAPI.takeRequest() val body = bodyFromRequest(request) - assertThat(request.target, Matchers.equalTo("/me/v1/authentication-methods")) + assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods")) assertThat(request.method, Matchers.equalTo("POST")) assertThat(body, Matchers.hasEntry("type", "push-notification" as Any)) } private inline fun bodyFromRequest(request: RecordedRequest): Map { val mapType = object : TypeToken>() {}.type - return gson.fromJson(request.body!!.utf8(), mapType) + return gson.fromJson(request.body.readUtf8(), mapType) } private val auth0: Auth0 diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index ea0976730..9eea3b9e2 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -45,6 +45,7 @@ import org.hamcrest.core.IsNot.not import org.hamcrest.core.IsNull.notNullValue import org.junit.Assert import org.junit.Before +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers @@ -56,7 +57,6 @@ import org.mockito.MockitoAnnotations import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -import org.robolectric.annotation.ConscryptMode import org.robolectric.shadows.ShadowLooper import java.io.ByteArrayInputStream import java.io.InputStream @@ -1538,17 +1538,18 @@ public class WebAuthProviderTest { } - @ConscryptMode(ConscryptMode.Mode.OFF) + // TODO: https://auth0team.atlassian.net/browse/SDK-7752 @Test + @Ignore("Fix these failing tests in CI once Roboelectric and other dependencies are updated") @Throws(Exception::class) public fun shouldFailToResumeLoginWhenRSAKeyIsMissingFromJWKSet() { val pkce = Mockito.mock(PKCE::class.java) `when`(pkce.codeChallenge).thenReturn("challenge") - val networkingClient: NetworkingClient = Mockito.spy(DefaultClient()) + val mockAPI = AuthenticationAPIMockServer() + mockAPI.willReturnEmptyJsonWebKeys() val authCallback = mock>() - val proxyAccount = - Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN) - proxyAccount.networkingClient = networkingClient + val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) + proxyAccount.networkingClient = SSLTestUtils.testClient login(proxyAccount) .withState("1234567890") .withNonce(JwtTestUtils.EXPECTED_NONCE) @@ -1585,15 +1586,8 @@ public class WebAuthProviderTest { callbackCaptor.firstValue.onSuccess(codeCredentials) null }.`when`(pkce).getToken(eq("1234"), callbackCaptor.capture()) - // Mock JWKS response with empty keys (no matching RSA key for kid) - val emptyJwksJson = """{"keys": []}""" - val jwksInputStream: InputStream = ByteArrayInputStream(emptyJwksJson.toByteArray()) - val jwksResponse = ServerResponse(200, jwksInputStream, emptyMap()) - Mockito.doReturn(jwksResponse).`when`(networkingClient).load( - eq(proxyAccount.getDomainUrl() + ".well-known/jwks.json"), - any() - ) Assert.assertTrue(resume(intent)) + mockAPI.takeRequest() ShadowLooper.idleMainLooper() verify(authCallback).onFailure(authExceptionCaptor.capture()) val error = authExceptionCaptor.firstValue @@ -1609,6 +1603,7 @@ public class WebAuthProviderTest { error.cause?.message, `is`("Could not find a public key for kid \"key123\"") ) + mockAPI.shutdown() } @Test @@ -1678,17 +1673,18 @@ public class WebAuthProviderTest { } - @ConscryptMode(ConscryptMode.Mode.OFF) + //TODO: https://auth0team.atlassian.net/browse/SDK-7752 @Test + @Ignore("Fix these failing tests in CI once Roboelectric and other dependencies are updated") @Throws(Exception::class) public fun shouldFailToResumeLoginWhenKeyIdIsMissingFromIdTokenHeader() { val pkce = Mockito.mock(PKCE::class.java) `when`(pkce.codeChallenge).thenReturn("challenge") - val networkingClient: NetworkingClient = Mockito.spy(DefaultClient()) + val mockAPI = AuthenticationAPIMockServer() + mockAPI.willReturnValidJsonWebKeys() val authCallback = mock>() - val proxyAccount = - Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN) - proxyAccount.networkingClient = networkingClient + val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) + proxyAccount.networkingClient = SSLTestUtils.testClient login(proxyAccount) .withState("1234567890") .withNonce("abcdefg") @@ -1724,15 +1720,8 @@ public class WebAuthProviderTest { callbackCaptor.firstValue.onSuccess(codeCredentials) null }.`when`(pkce).getToken(eq("1234"), callbackCaptor.capture()) - // Mock JWKS response with valid keys - val encoded = Files.readAllBytes(Paths.get("src/test/resources/rsa_jwks.json")) - val jwksInputStream: InputStream = ByteArrayInputStream(encoded) - val jwksResponse = ServerResponse(200, jwksInputStream, emptyMap()) - Mockito.doReturn(jwksResponse).`when`(networkingClient).load( - eq(proxyAccount.getDomainUrl() + ".well-known/jwks.json"), - any() - ) Assert.assertTrue(resume(intent)) + mockAPI.takeRequest() ShadowLooper.idleMainLooper() verify(authCallback).onFailure(authExceptionCaptor.capture()) val error = authExceptionCaptor.firstValue @@ -1748,6 +1737,7 @@ public class WebAuthProviderTest { error.cause?.message, `is`("Could not find a public key for kid \"null\"") ) + mockAPI.shutdown() } @Test diff --git a/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt b/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt index b02755f11..7a71a70bf 100644 --- a/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt @@ -6,9 +6,9 @@ import com.google.gson.Gson import com.google.gson.reflect.TypeToken import okhttp3.Interceptor import okhttp3.logging.HttpLoggingInterceptor -import mockwebserver3.MockResponse -import mockwebserver3.MockWebServer -import mockwebserver3.RecordedRequest +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.CoreMatchers.* import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.empty @@ -51,7 +51,7 @@ public class DefaultClientTest { @After public fun tearDown() { - mockServer.close() + mockServer.shutdown() } @Test @@ -274,7 +274,7 @@ public class DefaultClientTest { method: HttpMethod, headers: Map = mapOf("a-header" to "b-value") ) { - val requestUri = Uri.parse(request.target) + val requestUri = Uri.parse(request.path) when (method) { HttpMethod.GET -> assertThat(request.method, equalTo("GET")) HttpMethod.POST -> assertThat(request.method, equalTo("POST")) @@ -331,11 +331,10 @@ public class DefaultClientTest { } private fun enqueueMockResponse(responseCode: Int = STATUS_SUCCESS, jsonBody: String) { - val response = MockResponse.Builder() - .body(jsonBody) - .code(responseCode) - .setHeader("content-type", "application/json") - .build() + val response = MockResponse() + response.setBody(jsonBody) + response.setResponseCode(responseCode) + response.setHeader("content-type", "application/json") mockServer.enqueue(response) } @@ -345,7 +344,7 @@ public class DefaultClientTest { .collect(Collectors.joining("\n")) private fun RecordedRequest.bodyFromJson(): Map { - val text = this.body?.utf8() ?: "" + val text = this.body.readUtf8() if (text.isEmpty()) { return emptyMap() } diff --git a/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java b/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java index 17100bf9f..0c096ba3f 100644 --- a/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java +++ b/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java @@ -26,9 +26,4 @@ public ThreadSwitcherShadow() { public void backgroundThread(Runnable runnable) { executor.execute(runnable); } - - @Implementation - public void mainThread(Runnable runnable) { - executor.execute(runnable); - } } diff --git a/auth0/src/test/java/com/auth0/android/util/APIMockServer.kt b/auth0/src/test/java/com/auth0/android/util/APIMockServer.kt index 23f808e31..de8ddd305 100644 --- a/auth0/src/test/java/com/auth0/android/util/APIMockServer.kt +++ b/auth0/src/test/java/com/auth0/android/util/APIMockServer.kt @@ -1,8 +1,8 @@ package com.auth0.android.util -import mockwebserver3.MockResponse -import mockwebserver3.MockWebServer -import mockwebserver3.RecordedRequest +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest import java.io.IOException internal abstract class APIMockServer { @@ -12,7 +12,7 @@ internal abstract class APIMockServer { @Throws(IOException::class) fun shutdown() { - server.close() + server.shutdown() } @Throws(InterruptedException::class) @@ -21,23 +21,22 @@ internal abstract class APIMockServer { } fun responseWithJSON(json: String, statusCode: Int): MockResponse { - return MockResponse.Builder() - .code(statusCode) + return MockResponse() + .setResponseCode(statusCode) .addHeader("Content-Type", "application/json") - .body(json) - .build() + .setBody(json) } fun responseWithJSON(json: String, statusCode: Int, header: Map): MockResponse { - val builder = MockResponse.Builder() - .code(statusCode) + val response = MockResponse() + .setResponseCode(statusCode) .addHeader("Content-Type", "application/json") - .body(json) + .setBody(json) header.forEach { (key, value) -> - builder.addHeader(key, value) + response.addHeader(key, value) } - return builder.build() + return response } init { diff --git a/auth0/src/test/java/com/auth0/android/util/AuthenticationAPIMockServer.kt b/auth0/src/test/java/com/auth0/android/util/AuthenticationAPIMockServer.kt index 10375ec90..e08361e57 100755 --- a/auth0/src/test/java/com/auth0/android/util/AuthenticationAPIMockServer.kt +++ b/auth0/src/test/java/com/auth0/android/util/AuthenticationAPIMockServer.kt @@ -1,6 +1,6 @@ package com.auth0.android.util -import mockwebserver3.MockResponse +import okhttp3.mockwebserver.MockResponse import java.nio.file.Files import java.nio.file.Paths @@ -189,17 +189,15 @@ internal class AuthenticationAPIMockServer : APIMockServer() { } private fun responseEmpty(statusCode: Int): MockResponse { - return MockResponse.Builder() - .code(statusCode) - .build() + return MockResponse() + .setResponseCode(statusCode) } private fun responseWithPlainText(statusMessage: String, statusCode: Int): MockResponse { - return MockResponse.Builder() - .code(statusCode) + return MockResponse() + .setResponseCode(statusCode) .addHeader("Content-Type", "text/plain") - .body(statusMessage) - .build() + .setBody(statusMessage) } companion object { diff --git a/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt b/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt index 617301299..542a8e4b6 100644 --- a/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt +++ b/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt @@ -2,7 +2,7 @@ package com.auth0.android.util import com.auth0.android.request.DefaultClient import com.auth0.android.request.internal.GsonProvider -import mockwebserver3.MockWebServer +import okhttp3.mockwebserver.MockWebServer import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import java.net.InetAddress @@ -44,7 +44,7 @@ internal object SSLTestUtils { fun createMockWebServer(): MockWebServer { val mockServer = MockWebServer() - mockServer.useHttps(serverCertificates.sslSocketFactory()) + mockServer.useHttps(serverCertificates.sslSocketFactory(), false) return mockServer } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index d01397000..7f94c35eb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "2.2.0" + ext.kotlin_version = "2.0.21" repositories { google() mavenCentral() From d2fbec226c00885c33647cf361120bfb95ce9791 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Wed, 11 Feb 2026 15:56:51 +0530 Subject: [PATCH 15/28] feat: add Builder pattern for DefaultClient with deprecated legacy constructor --- EXAMPLES.md | 102 +++++- V4_MIGRATION_GUIDE.md | 31 ++ .../auth0/android/request/DefaultClient.kt | 254 ++++++++++++--- .../android/provider/WebAuthProviderTest.kt | 1 + .../android/request/DefaultClientTest.kt | 290 +++++++++++++++++- .../com/auth0/android/util/SSLTestUtils.kt | 20 +- .../com/auth0/sample/DatabaseLoginFragment.kt | 4 +- 7 files changed, 626 insertions(+), 76 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 1ad204be4..b97d02f81 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -3197,10 +3197,12 @@ The Auth0 class can be configured with a `NetworkingClient`, which will be used ### Timeout configuration ```kotlin -val netClient = DefaultClient( - connectTimeout = 30, - readTimeout = 30 -) +val netClient = DefaultClient.Builder() + .connectTimeout(30) + .readTimeout(30) + .writeTimeout(30) + .callTimeout(120) + .build() val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") account.networkingClient = netClient @@ -3210,43 +3212,78 @@ account.networkingClient = netClient Using Java ```java -DefaultClient netClient = new DefaultClient(30, 30); +DefaultClient netClient = new DefaultClient.Builder() + .connectTimeout(30) + .readTimeout(30) + .writeTimeout(30) + .callTimeout(120) + .build(); Auth0 account = Auth0.getInstance("client id", "domain"); account.setNetworkingClient(netClient); ``` -### Logging configuration +
+ Legacy constructor (still supported) ```kotlin val netClient = DefaultClient( - enableLogging = true + connectTimeout = 30, + readTimeout = 30 ) +``` +
+ +### Logging configuration + +```kotlin +val netClient = DefaultClient.Builder() + .enableLogging(true) + .build() val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") account.networkingClient = netClient ``` +You can also customize the log level and provide a custom logger: + +```kotlin +val netClient = DefaultClient.Builder() + .enableLogging(true) + .logLevel(HttpLoggingInterceptor.Level.HEADERS) // NONE, BASIC, HEADERS, or BODY (default) + .logger(HttpLoggingInterceptor.Logger { message -> Log.d("Auth0Http", message) }) + .build() +``` +
Using Java ```java -import java.util.HashMap; - -DefaultClient netClient = new DefaultClient( - 10, 10, new HashMap<>() ,true -); +DefaultClient netClient = new DefaultClient.Builder() + .enableLogging(true) + .logLevel(HttpLoggingInterceptor.Level.HEADERS) + .build(); Auth0 account = Auth0.getInstance("client id", "domain"); account.setNetworkingClient(netClient); ```
-### Set additional headers for all requests +
+ Legacy constructor (still supported) ```kotlin val netClient = DefaultClient( - defaultHeaders = mapOf("{HEADER-NAME}" to "{HEADER-VALUE}") + enableLogging = true ) +``` +
+ +### Set additional headers for all requests + +```kotlin +val netClient = DefaultClient.Builder() + .defaultHeaders(mapOf("{HEADER-NAME}" to "{HEADER-VALUE}")) + .build() val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") account.networkingClient = netClient @@ -3259,14 +3296,45 @@ account.networkingClient = netClient Map defaultHeaders = new HashMap<>(); defaultHeaders.put("{HEADER-NAME}", "{HEADER-VALUE}"); -DefaultClient netClient = new DefaultClient( - 10,10 , defaultHeaders -); +DefaultClient netClient = new DefaultClient.Builder() + .defaultHeaders(defaultHeaders) + .build(); Auth0 account = Auth0.getInstance("client id", "domain"); account.setNetworkingClient(netClient); ``` +
+ Legacy constructor (still supported) + +```kotlin +val netClient = DefaultClient( + defaultHeaders = mapOf("{HEADER-NAME}" to "{HEADER-VALUE}") +) +``` +
+ +### Custom interceptors + +You can add custom OkHttp interceptors to the `DefaultClient` for use cases such as auth token injection, analytics, or certificate pinning: + +```kotlin +val netClient = DefaultClient.Builder() + .addInterceptor(Interceptor { chain -> + val request = chain.request().newBuilder() + .addHeader("X-Request-Id", UUID.randomUUID().toString()) + .build() + chain.proceed(request) + }) + .addInterceptor(myAnalyticsInterceptor) + .build() + +val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") +account.networkingClient = netClient +``` + +Interceptors are invoked in the order they were added, after the built-in retry interceptor and before the logging interceptor. + ### Advanced configuration For more advanced configuration of the networking client, you can provide a custom implementation of `NetworkingClient`. This may be useful when you wish to reuse your own networking client, configure a proxy, etc. diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md index b8ca744b3..97d80c09c 100644 --- a/V4_MIGRATION_GUIDE.md +++ b/V4_MIGRATION_GUIDE.md @@ -96,6 +96,37 @@ implementation 'com.google.code.gson:gson:2.8.9' // your preferred version > **Note:** Pinning or excluding is not recommended long-term, as the SDK has been tested and validated against Gson 2.11.0. +### DefaultClient.Builder + +v4 introduces a `DefaultClient.Builder` for configuring the HTTP client. This replaces the constructor-based approach with a more flexible builder pattern that supports additional options such as write/call timeouts, custom interceptors, and custom loggers. + +**v3 (constructor-based — deprecated):** + +```kotlin +// ⚠️ Deprecated: still compiles but shows a warning +val client = DefaultClient( + connectTimeout = 30, + readTimeout = 30, + enableLogging = true +) +``` + +**v4 (builder pattern — recommended):** + +```kotlin +val client = DefaultClient.Builder() + .connectTimeout(30) + .readTimeout(30) + .writeTimeout(30) + .callTimeout(120) + .enableLogging(true) + .logLevel(HttpLoggingInterceptor.Level.HEADERS) + .addInterceptor(myCustomInterceptor) + .build() +``` + +The legacy constructor is deprecated but **not removed** — existing code will continue to compile and run. Your IDE will show a deprecation warning with a suggested `ReplaceWith` quick-fix to migrate to the Builder. + ## Getting Help If you encounter issues during migration: diff --git a/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt b/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt index 157f4f6b7..0b1b4d74e 100644 --- a/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt +++ b/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt @@ -24,25 +24,187 @@ import javax.net.ssl.X509TrustManager /** * Default implementation of a Networking Client. + * + * Use [DefaultClient.Builder] to create a new instance with custom configuration: + * + * ```kotlin + * val client = DefaultClient.Builder() + * .connectTimeout(30) + * .readTimeout(30) + * .writeTimeout(30) + * .enableLogging(true) + * .addInterceptor(myInterceptor) + * .build() + * ``` + * + * The legacy constructor-based API is still supported for backward compatibility. */ -public class DefaultClient @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal constructor( - connectTimeout: Int, - readTimeout: Int, +public class DefaultClient private constructor( private val defaultHeaders: Map, - enableLogging: Boolean, private val gson: Gson, - sslSocketFactory: SSLSocketFactory?, - trustManager: X509TrustManager? + okHttpClientBuilder: OkHttpClient.Builder ) : NetworkingClient { /** - * Create a new DefaultClient. + * Builder for creating a [DefaultClient] instance with custom configuration. * - * @param connectTimeout the connection timeout, in seconds, to use when executing requests. Default is ten seconds. - * @param readTimeout the read timeout, in seconds, to use when executing requests. Default is ten seconds. - * @param defaultHeaders any headers that should be sent on all requests. If a specific request specifies a header with the same key as any header in the default headers, the header specified on the request will take precedence. Default is an empty map. - * @param enableLogging whether HTTP request and response info should be logged. This should only be set to `true` for debugging purposes in non-production environments, as sensitive information is included in the logs. Defaults to `false`. + * Example usage: + * ```kotlin + * val client = DefaultClient.Builder() + * .connectTimeout(30) + * .readTimeout(30) + * .writeTimeout(30) + * .callTimeout(60) + * .defaultHeaders(mapOf("X-Custom" to "value")) + * .enableLogging(true) + * .logLevel(HttpLoggingInterceptor.Level.HEADERS) + * .logger(myCustomLogger) + * .addInterceptor(myInterceptor) + * .build() + * ``` */ + public class Builder { + private var connectTimeout: Int = DEFAULT_TIMEOUT_SECONDS + private var readTimeout: Int = DEFAULT_TIMEOUT_SECONDS + private var writeTimeout: Int = DEFAULT_TIMEOUT_SECONDS + private var callTimeout: Int = 0 + private var defaultHeaders: Map = mapOf() + private var enableLogging: Boolean = false + private var logLevel: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.BODY + private var logger: HttpLoggingInterceptor.Logger? = null + private val interceptors: MutableList = mutableListOf() + private var gson: Gson = GsonProvider.gson + private var sslSocketFactory: SSLSocketFactory? = null + private var trustManager: X509TrustManager? = null + + /** + * Sets the connection timeout, in seconds. Default is 10 seconds. + */ + public fun connectTimeout(timeout: Int): Builder = apply { this.connectTimeout = timeout } + + /** + * Sets the read timeout, in seconds. Default is 10 seconds. + */ + public fun readTimeout(timeout: Int): Builder = apply { this.readTimeout = timeout } + + /** + * Sets the write timeout, in seconds. Default is 10 seconds. + */ + public fun writeTimeout(timeout: Int): Builder = apply { this.writeTimeout = timeout } + + /** + * Sets the call timeout, in seconds. Default is 0 (no timeout). + * This is an overall timeout that spans the entire call: resolving DNS, connecting, + * writing the request body, server processing, and reading the response body. + */ + public fun callTimeout(timeout: Int): Builder = apply { this.callTimeout = timeout } + + /** + * Sets default headers to include on all requests. If a specific request specifies + * a header with the same key, the request-level header takes precedence. + */ + public fun defaultHeaders(headers: Map): Builder = + apply { this.defaultHeaders = headers } + + /** + * Enables or disables HTTP logging. Should only be set to `true` for debugging + * in non-production environments, as sensitive information may be logged. + * Defaults to `false`. + */ + public fun enableLogging(enable: Boolean): Builder = apply { this.enableLogging = enable } + + /** + * Sets the log level for the HTTP logging interceptor. + * Only takes effect if [enableLogging] is set to `true`. + * Default is [HttpLoggingInterceptor.Level.BODY]. + */ + public fun logLevel(level: HttpLoggingInterceptor.Level): Builder = + apply { this.logLevel = level } + + /** + * Sets a custom logger for the HTTP logging interceptor. + * Only takes effect if [enableLogging] is set to `true`. + * If not set, the default [HttpLoggingInterceptor.Logger] (which logs to logcat) is used. + */ + public fun logger(logger: HttpLoggingInterceptor.Logger): Builder = + apply { this.logger = logger } + + /** + * Adds a custom OkHttp [Interceptor]. Multiple interceptors can be added + * by calling this method multiple times. They are invoked in the order they were added, + * after the built-in [RetryInterceptor] and before the logging interceptor. + */ + public fun addInterceptor(interceptor: Interceptor): Builder = + apply { this.interceptors.add(interceptor) } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun gson(gson: Gson): Builder = apply { this.gson = gson } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun sslSocketFactory( + sslSocketFactory: SSLSocketFactory, + trustManager: X509TrustManager + ): Builder = apply { + this.sslSocketFactory = sslSocketFactory + this.trustManager = trustManager + } + + /** + * Builds a new [DefaultClient] instance with the configured options. + */ + public fun build(): DefaultClient { + val okBuilder = OkHttpClient.Builder() + + okBuilder.addInterceptor(RetryInterceptor()) + + interceptors.forEach { okBuilder.addInterceptor(it) } + + if (enableLogging) { + val loggingInterceptor = if (logger != null) { + HttpLoggingInterceptor(logger!!) + } else { + HttpLoggingInterceptor() + } + loggingInterceptor.setLevel(logLevel) + okBuilder.addInterceptor(loggingInterceptor) + } + + okBuilder.connectTimeout(connectTimeout.toLong(), TimeUnit.SECONDS) + okBuilder.readTimeout(readTimeout.toLong(), TimeUnit.SECONDS) + okBuilder.writeTimeout(writeTimeout.toLong(), TimeUnit.SECONDS) + okBuilder.callTimeout(callTimeout.toLong(), TimeUnit.SECONDS) + + val ssl = sslSocketFactory + val tm = trustManager + if (ssl != null && tm != null) { + okBuilder.sslSocketFactory(ssl, tm) + } + + return DefaultClient(defaultHeaders, gson, okBuilder) + } + } + + /** + * Create a new DefaultClient with default configuration. + * + * For more configuration options, use [DefaultClient.Builder]. + * + * @param connectTimeout the connection timeout, in seconds. Default is 10 seconds. + * @param readTimeout the read timeout, in seconds. Default is 10 seconds. + * @param defaultHeaders headers to include on all requests. Default is an empty map. + * @param enableLogging whether to log HTTP request/response info. Defaults to `false`. + */ + @Deprecated( + message = "Use DefaultClient.Builder() for more configuration options.", + replaceWith = ReplaceWith( + "DefaultClient.Builder()" + + ".connectTimeout(connectTimeout)" + + ".readTimeout(readTimeout)" + + ".defaultHeaders(defaultHeaders)" + + ".enableLogging(enableLogging)" + + ".build()" + ) + ) @JvmOverloads public constructor( connectTimeout: Int = DEFAULT_TIMEOUT_SECONDS, @@ -50,13 +212,44 @@ public class DefaultClient @VisibleForTesting(otherwise = VisibleForTesting.PRIV defaultHeaders: Map = mapOf(), enableLogging: Boolean = false, ) : this( - connectTimeout, - readTimeout, - defaultHeaders, - enableLogging, - GsonProvider.gson, - null, - null + defaultHeaders = defaultHeaders, + gson = GsonProvider.gson, + okHttpClientBuilder = OkHttpClient.Builder().apply { + addInterceptor(RetryInterceptor()) + if (enableLogging) { + addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) + } + connectTimeout(connectTimeout.toLong(), TimeUnit.SECONDS) + readTimeout(readTimeout.toLong(), TimeUnit.SECONDS) + } + ) + + /** + * Internal constructor used by tests to inject SSL and Gson. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal constructor( + connectTimeout: Int, + readTimeout: Int, + defaultHeaders: Map, + enableLogging: Boolean, + gson: Gson, + sslSocketFactory: SSLSocketFactory?, + trustManager: X509TrustManager? + ) : this( + defaultHeaders = defaultHeaders, + gson = gson, + okHttpClientBuilder = OkHttpClient.Builder().apply { + addInterceptor(RetryInterceptor()) + if (enableLogging) { + addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) + } + connectTimeout(connectTimeout.toLong(), TimeUnit.SECONDS) + readTimeout(readTimeout.toLong(), TimeUnit.SECONDS) + if (sslSocketFactory != null && trustManager != null) { + sslSocketFactory(sslSocketFactory, trustManager) + } + } ) @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @@ -125,35 +318,14 @@ public class DefaultClient @VisibleForTesting(otherwise = VisibleForTesting.PRIV } init { - val builder = OkHttpClient.Builder() - // Add retry interceptor - builder.addInterceptor(RetryInterceptor()) - - // logging - if (enableLogging) { - val logger: Interceptor = HttpLoggingInterceptor() - .setLevel(HttpLoggingInterceptor.Level.BODY) - builder.addInterceptor(logger) - } + okHttpClient = okHttpClientBuilder.build() - // timeouts - builder.connectTimeout(connectTimeout.toLong(), TimeUnit.SECONDS) - builder.readTimeout(readTimeout.toLong(), TimeUnit.SECONDS) - - // testing with ssl hook (internal constructor params visibility only) - if (sslSocketFactory != null && trustManager != null) { - builder.sslSocketFactory(sslSocketFactory, trustManager) - } - - okHttpClient = builder.build() - - // Non-retryable client for DPoP requests + // Non-retryable client for DPoP requests — inherits all configuration nonRetryableOkHttpClient = okHttpClient.newBuilder() .retryOnConnectionFailure(false) .build() } - internal companion object { const val DEFAULT_TIMEOUT_SECONDS: Int = 10 val APPLICATION_JSON_UTF8: MediaType = diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index 9eea3b9e2..94baa6b3f 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -1169,6 +1169,7 @@ public class WebAuthProviderTest { @Test @Throws(Exception::class) + @Suppress("DEPRECATION") public fun shouldResumeLoginWithCustomNetworkingClient() { val networkingClient: NetworkingClient = Mockito.spy(DefaultClient()) val authCallback = mock>() diff --git a/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt b/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt index 7a71a70bf..0db1c95a0 100644 --- a/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt @@ -81,6 +81,7 @@ public class DefaultClientTest { } @Test + @Suppress("DEPRECATION") public fun shouldHaveLoggingDisabledByDefault() { val netClient = DefaultClient(enableLogging = false) assertThat(netClient.okHttpClient.interceptors, hasSize(1)) @@ -91,6 +92,7 @@ public class DefaultClientTest { } @Test + @Suppress("DEPRECATION") public fun shouldHaveRetryInterceptorEnabled() { val netClient = DefaultClient(enableLogging = false) assertThat(netClient.okHttpClient.interceptors, hasSize(1)) @@ -101,6 +103,7 @@ public class DefaultClientTest { } @Test + @Suppress("DEPRECATION") public fun shouldHaveLoggingEnabledIfSpecified() { val netClient = DefaultClient(enableLogging = true) assertThat(netClient.okHttpClient.interceptors, hasSize(2)) @@ -113,6 +116,7 @@ public class DefaultClientTest { } @Test + @Suppress("DEPRECATION") public fun shouldHaveDefaultTimeoutValues() { val client = DefaultClient() assertThat(client.okHttpClient.connectTimeoutMillis, equalTo(10 * 1000)) @@ -120,6 +124,7 @@ public class DefaultClientTest { } @Test + @Suppress("DEPRECATION") public fun shouldUseTimeoutConfigIfSpecified() { val client = DefaultClient(connectTimeout = 100, readTimeout = 200) assertThat(client.okHttpClient.connectTimeoutMillis, equalTo(100 * 1000)) @@ -353,14 +358,283 @@ public class DefaultClientTest { } private fun createDefaultClientForTest(defaultHeaders: Map): DefaultClient { - return DefaultClient( - defaultHeaders = defaultHeaders, - readTimeout = 10, - connectTimeout = 10, - enableLogging = false, - gson = gson, - sslSocketFactory = SSLTestUtils.clientCertificates.sslSocketFactory(), - trustManager = SSLTestUtils.clientCertificates.trustManager + return DefaultClient.Builder() + .connectTimeout(10) + .readTimeout(10) + .defaultHeaders(defaultHeaders) + .enableLogging(false) + .gson(gson) + .sslSocketFactory( + SSLTestUtils.clientCertificates.sslSocketFactory(), + SSLTestUtils.clientCertificates.trustManager + ) + .build() + } + + @Test + public fun builderShouldCreateClientWithDefaultValues() { + val client = DefaultClient.Builder().build() + assertThat(client.okHttpClient.connectTimeoutMillis, equalTo(10 * 1000)) + assertThat(client.okHttpClient.readTimeoutMillis, equalTo(10 * 1000)) + assertThat(client.okHttpClient.writeTimeoutMillis, equalTo(10 * 1000)) + assertThat(client.okHttpClient.callTimeoutMillis, equalTo(0)) + assertThat(client.okHttpClient.interceptors, hasSize(1)) + assertThat(client.okHttpClient.interceptors[0] is RetryInterceptor, equalTo(true)) + } + + @Test + public fun builderShouldSetConnectTimeout() { + val client = DefaultClient.Builder() + .connectTimeout(30) + .build() + assertThat(client.okHttpClient.connectTimeoutMillis, equalTo(30 * 1000)) + } + + @Test + public fun builderShouldSetReadTimeout() { + val client = DefaultClient.Builder() + .readTimeout(45) + .build() + assertThat(client.okHttpClient.readTimeoutMillis, equalTo(45 * 1000)) + } + + @Test + public fun builderShouldSetWriteTimeout() { + val client = DefaultClient.Builder() + .writeTimeout(20) + .build() + assertThat(client.okHttpClient.writeTimeoutMillis, equalTo(20 * 1000)) + } + + @Test + public fun builderShouldSetCallTimeout() { + val client = DefaultClient.Builder() + .callTimeout(60) + .build() + assertThat(client.okHttpClient.callTimeoutMillis, equalTo(60 * 1000)) + } + + @Test + public fun builderShouldSetAllTimeouts() { + val client = DefaultClient.Builder() + .connectTimeout(15) + .readTimeout(25) + .writeTimeout(35) + .callTimeout(120) + .build() + assertThat(client.okHttpClient.connectTimeoutMillis, equalTo(15 * 1000)) + assertThat(client.okHttpClient.readTimeoutMillis, equalTo(25 * 1000)) + assertThat(client.okHttpClient.writeTimeoutMillis, equalTo(35 * 1000)) + assertThat(client.okHttpClient.callTimeoutMillis, equalTo(120 * 1000)) + } + + @Test + public fun builderShouldEnableLoggingWithDefaultLevel() { + val client = DefaultClient.Builder() + .enableLogging(true) + .build() + assertThat(client.okHttpClient.interceptors, hasSize(2)) + val loggingInterceptor = client.okHttpClient.interceptors[1] as HttpLoggingInterceptor + assertThat(loggingInterceptor.level, equalTo(HttpLoggingInterceptor.Level.BODY)) + } + + @Test + public fun builderShouldSetCustomLogLevel() { + val client = DefaultClient.Builder() + .enableLogging(true) + .logLevel(HttpLoggingInterceptor.Level.HEADERS) + .build() + assertThat(client.okHttpClient.interceptors, hasSize(2)) + val loggingInterceptor = client.okHttpClient.interceptors[1] as HttpLoggingInterceptor + assertThat(loggingInterceptor.level, equalTo(HttpLoggingInterceptor.Level.HEADERS)) + } + + @Test + public fun builderShouldNotAddLoggingInterceptorWhenDisabled() { + val client = DefaultClient.Builder() + .enableLogging(false) + .logLevel(HttpLoggingInterceptor.Level.HEADERS) + .build() + assertThat(client.okHttpClient.interceptors, hasSize(1)) + assertThat(client.okHttpClient.interceptors[0] is RetryInterceptor, equalTo(true)) + } + + @Test + public fun builderShouldSetCustomLogger() { + val logs = mutableListOf() + val customLogger = HttpLoggingInterceptor.Logger { message -> logs.add(message) } + + val client = DefaultClient.Builder() + .enableLogging(true) + .logger(customLogger) + .sslSocketFactory( + SSLTestUtils.clientCertificates.sslSocketFactory(), + SSLTestUtils.clientCertificates.trustManager + ) + .build() + + assertThat(client.okHttpClient.interceptors, hasSize(2)) + assertThat(client.okHttpClient.interceptors[1] is HttpLoggingInterceptor, equalTo(true)) + + enqueueMockResponse(STATUS_SUCCESS, JSON_OK) + executeRequest(HttpMethod.GET, client) + assertThat(logs.isEmpty(), equalTo(false)) + } + + @Test + public fun builderShouldAddSingleCustomInterceptor() { + var intercepted = false + val customInterceptor = Interceptor { chain -> + intercepted = true + chain.proceed(chain.request()) + } + + val client = DefaultClient.Builder() + .addInterceptor(customInterceptor) + .sslSocketFactory( + SSLTestUtils.clientCertificates.sslSocketFactory(), + SSLTestUtils.clientCertificates.trustManager + ) + .build() + + assertThat(client.okHttpClient.interceptors, hasSize(2)) + assertThat(client.okHttpClient.interceptors[0] is RetryInterceptor, equalTo(true)) + + enqueueMockResponse(STATUS_SUCCESS, JSON_OK) + executeRequest(HttpMethod.GET, client) + assertThat(intercepted, equalTo(true)) + } + + @Test + public fun builderShouldAddMultipleCustomInterceptors() { + val callOrder = mutableListOf() + val firstInterceptor = Interceptor { chain -> + callOrder.add("first") + chain.proceed(chain.request()) + } + val secondInterceptor = Interceptor { chain -> + callOrder.add("second") + chain.proceed(chain.request()) + } + + val client = DefaultClient.Builder() + .addInterceptor(firstInterceptor) + .addInterceptor(secondInterceptor) + .sslSocketFactory( + SSLTestUtils.clientCertificates.sslSocketFactory(), + SSLTestUtils.clientCertificates.trustManager + ) + .build() + + assertThat(client.okHttpClient.interceptors, hasSize(3)) + + enqueueMockResponse(STATUS_SUCCESS, JSON_OK) + executeRequest(HttpMethod.GET, client) + assertThat(callOrder, equalTo(listOf("first", "second"))) + } + + @Test + public fun builderShouldPlaceCustomInterceptorsBeforeLogging() { + val callOrder = mutableListOf() + val customInterceptor = Interceptor { chain -> + callOrder.add("custom") + chain.proceed(chain.request()) + } + val customLogger = HttpLoggingInterceptor.Logger { callOrder.add("logging") } + + val client = DefaultClient.Builder() + .addInterceptor(customInterceptor) + .enableLogging(true) + .logger(customLogger) + .sslSocketFactory( + SSLTestUtils.clientCertificates.sslSocketFactory(), + SSLTestUtils.clientCertificates.trustManager + ) + .build() + + assertThat(client.okHttpClient.interceptors, hasSize(3)) + assertThat(client.okHttpClient.interceptors[0] is RetryInterceptor, equalTo(true)) + assertThat(client.okHttpClient.interceptors[2] is HttpLoggingInterceptor, equalTo(true)) + + enqueueMockResponse(STATUS_SUCCESS, JSON_OK) + executeRequest(HttpMethod.GET, client) + val customIndex = callOrder.indexOf("custom") + val loggingIndex = callOrder.indexOf("logging") + assertThat(customIndex, not(equalTo(-1))) + assertThat(loggingIndex, not(equalTo(-1))) + assertThat(customIndex < loggingIndex, equalTo(true)) + } + + @Test + public fun builderShouldSetDefaultHeaders() { + enqueueMockResponse(STATUS_SUCCESS, JSON_OK) + val client = DefaultClient.Builder() + .defaultHeaders(mapOf("x-custom" to "test-value")) + .sslSocketFactory( + SSLTestUtils.clientCertificates.sslSocketFactory(), + SSLTestUtils.clientCertificates.trustManager + ) + .build() + + executeRequest(HttpMethod.GET, client) + val sentRequest = mockServer.takeRequest() + requestAssertions(sentRequest, HttpMethod.GET, mapOf("x-custom" to "test-value")) + } + + @Test + public fun builderNonRetryableClientShouldInheritConfiguration() { + val customInterceptor = Interceptor { chain -> chain.proceed(chain.request()) } + val client = DefaultClient.Builder() + .connectTimeout(25) + .readTimeout(35) + .writeTimeout(45) + .addInterceptor(customInterceptor) + .enableLogging(true) + .build() + + assertThat( + client.nonRetryableOkHttpClient.connectTimeoutMillis, + equalTo(client.okHttpClient.connectTimeoutMillis) + ) + assertThat( + client.nonRetryableOkHttpClient.readTimeoutMillis, + equalTo(client.okHttpClient.readTimeoutMillis) + ) + assertThat( + client.nonRetryableOkHttpClient.writeTimeoutMillis, + equalTo(client.okHttpClient.writeTimeoutMillis) + ) + assertThat( + client.nonRetryableOkHttpClient.interceptors.size, + equalTo(client.okHttpClient.interceptors.size) + ) + assertThat(client.okHttpClient.retryOnConnectionFailure, equalTo(true)) + assertThat(client.nonRetryableOkHttpClient.retryOnConnectionFailure, equalTo(false)) + } + + @Test + @Suppress("DEPRECATION") + public fun legacyConstructorShouldStillWork() { + val client = DefaultClient() + assertThat(client.okHttpClient.connectTimeoutMillis, equalTo(10 * 1000)) + assertThat(client.okHttpClient.readTimeoutMillis, equalTo(10 * 1000)) + assertThat(client.okHttpClient.interceptors, hasSize(1)) + assertThat(client.okHttpClient.interceptors[0] is RetryInterceptor, equalTo(true)) + } + + @Test + @Suppress("DEPRECATION") + public fun legacyConstructorWithParamsShouldStillWork() { + val client = DefaultClient( + connectTimeout = 30, + readTimeout = 45, + defaultHeaders = mapOf("X-Test" to "value"), + enableLogging = true ) + assertThat(client.okHttpClient.connectTimeoutMillis, equalTo(30 * 1000)) + assertThat(client.okHttpClient.readTimeoutMillis, equalTo(45 * 1000)) + assertThat(client.okHttpClient.interceptors, hasSize(2)) + assertThat(client.okHttpClient.interceptors[0] is RetryInterceptor, equalTo(true)) + assertThat(client.okHttpClient.interceptors[1] is HttpLoggingInterceptor, equalTo(true)) } } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt b/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt index 542a8e4b6..897d71123 100644 --- a/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt +++ b/auth0/src/test/java/com/auth0/android/util/SSLTestUtils.kt @@ -31,15 +31,17 @@ internal object SSLTestUtils { .heldCertificate(localhostCertificate) .build() - testClient = DefaultClient( - defaultHeaders = mapOf(), - readTimeout = 10, - connectTimeout = 10, - enableLogging = false, - gson = GsonProvider.gson, - sslSocketFactory = clientCertificates.sslSocketFactory(), - trustManager = clientCertificates.trustManager - ) + testClient = DefaultClient.Builder() + .connectTimeout(10) + .readTimeout(10) + .defaultHeaders(mapOf()) + .enableLogging(false) + .gson(GsonProvider.gson) + .sslSocketFactory( + clientCertificates.sslSocketFactory(), + clientCertificates.trustManager + ) + .build() } fun createMockWebServer(): MockWebServer { diff --git a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt index 918de531b..c4e54212a 100644 --- a/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt +++ b/sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt @@ -59,7 +59,9 @@ class DatabaseLoginFragment : Fragment() { getString(R.string.com_auth0_domain) ) // Only enable network traffic logging on production environments! - account.networkingClient = DefaultClient(enableLogging = true) + account.networkingClient = DefaultClient.Builder() + .enableLogging(true) + .build() account } From 991b49f49a886bb518065079256e17fd54d5b98d Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Fri, 13 Feb 2026 16:34:43 +0530 Subject: [PATCH 16/28] Resolved review comments --- .../auth0/android/request/DefaultClient.kt | 54 +--------- .../android/request/DefaultClientTest.kt | 99 ------------------- 2 files changed, 1 insertion(+), 152 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt b/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt index 0b1b4d74e..0ea2c46db 100644 --- a/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt +++ b/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt @@ -9,7 +9,6 @@ import okhttp3.Headers import okhttp3.Headers.Companion.toHeaders import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Interceptor import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient @@ -33,7 +32,6 @@ import javax.net.ssl.X509TrustManager * .readTimeout(30) * .writeTimeout(30) * .enableLogging(true) - * .addInterceptor(myInterceptor) * .build() * ``` * @@ -57,9 +55,7 @@ public class DefaultClient private constructor( * .callTimeout(60) * .defaultHeaders(mapOf("X-Custom" to "value")) * .enableLogging(true) - * .logLevel(HttpLoggingInterceptor.Level.HEADERS) * .logger(myCustomLogger) - * .addInterceptor(myInterceptor) * .build() * ``` */ @@ -70,9 +66,7 @@ public class DefaultClient private constructor( private var callTimeout: Int = 0 private var defaultHeaders: Map = mapOf() private var enableLogging: Boolean = false - private var logLevel: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.BODY private var logger: HttpLoggingInterceptor.Logger? = null - private val interceptors: MutableList = mutableListOf() private var gson: Gson = GsonProvider.gson private var sslSocketFactory: SSLSocketFactory? = null private var trustManager: X509TrustManager? = null @@ -113,14 +107,6 @@ public class DefaultClient private constructor( */ public fun enableLogging(enable: Boolean): Builder = apply { this.enableLogging = enable } - /** - * Sets the log level for the HTTP logging interceptor. - * Only takes effect if [enableLogging] is set to `true`. - * Default is [HttpLoggingInterceptor.Level.BODY]. - */ - public fun logLevel(level: HttpLoggingInterceptor.Level): Builder = - apply { this.logLevel = level } - /** * Sets a custom logger for the HTTP logging interceptor. * Only takes effect if [enableLogging] is set to `true`. @@ -129,14 +115,6 @@ public class DefaultClient private constructor( public fun logger(logger: HttpLoggingInterceptor.Logger): Builder = apply { this.logger = logger } - /** - * Adds a custom OkHttp [Interceptor]. Multiple interceptors can be added - * by calling this method multiple times. They are invoked in the order they were added, - * after the built-in [RetryInterceptor] and before the logging interceptor. - */ - public fun addInterceptor(interceptor: Interceptor): Builder = - apply { this.interceptors.add(interceptor) } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal fun gson(gson: Gson): Builder = apply { this.gson = gson } @@ -157,15 +135,13 @@ public class DefaultClient private constructor( okBuilder.addInterceptor(RetryInterceptor()) - interceptors.forEach { okBuilder.addInterceptor(it) } - if (enableLogging) { val loggingInterceptor = if (logger != null) { HttpLoggingInterceptor(logger!!) } else { HttpLoggingInterceptor() } - loggingInterceptor.setLevel(logLevel) + loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY) okBuilder.addInterceptor(loggingInterceptor) } @@ -224,34 +200,6 @@ public class DefaultClient private constructor( } ) - /** - * Internal constructor used by tests to inject SSL and Gson. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal constructor( - connectTimeout: Int, - readTimeout: Int, - defaultHeaders: Map, - enableLogging: Boolean, - gson: Gson, - sslSocketFactory: SSLSocketFactory?, - trustManager: X509TrustManager? - ) : this( - defaultHeaders = defaultHeaders, - gson = gson, - okHttpClientBuilder = OkHttpClient.Builder().apply { - addInterceptor(RetryInterceptor()) - if (enableLogging) { - addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) - } - connectTimeout(connectTimeout.toLong(), TimeUnit.SECONDS) - readTimeout(readTimeout.toLong(), TimeUnit.SECONDS) - if (sslSocketFactory != null && trustManager != null) { - sslSocketFactory(sslSocketFactory, trustManager) - } - } - ) - @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal val okHttpClient: OkHttpClient diff --git a/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt b/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt index 0db1c95a0..627a68641 100644 --- a/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt @@ -11,7 +11,6 @@ import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.CoreMatchers.* import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.empty import org.hamcrest.Matchers.hasSize import org.hamcrest.collection.IsMapContaining.hasEntry import org.hamcrest.collection.IsMapWithSize.anEmptyMap @@ -438,22 +437,10 @@ public class DefaultClientTest { assertThat(loggingInterceptor.level, equalTo(HttpLoggingInterceptor.Level.BODY)) } - @Test - public fun builderShouldSetCustomLogLevel() { - val client = DefaultClient.Builder() - .enableLogging(true) - .logLevel(HttpLoggingInterceptor.Level.HEADERS) - .build() - assertThat(client.okHttpClient.interceptors, hasSize(2)) - val loggingInterceptor = client.okHttpClient.interceptors[1] as HttpLoggingInterceptor - assertThat(loggingInterceptor.level, equalTo(HttpLoggingInterceptor.Level.HEADERS)) - } - @Test public fun builderShouldNotAddLoggingInterceptorWhenDisabled() { val client = DefaultClient.Builder() .enableLogging(false) - .logLevel(HttpLoggingInterceptor.Level.HEADERS) .build() assertThat(client.okHttpClient.interceptors, hasSize(1)) assertThat(client.okHttpClient.interceptors[0] is RetryInterceptor, equalTo(true)) @@ -481,90 +468,6 @@ public class DefaultClientTest { assertThat(logs.isEmpty(), equalTo(false)) } - @Test - public fun builderShouldAddSingleCustomInterceptor() { - var intercepted = false - val customInterceptor = Interceptor { chain -> - intercepted = true - chain.proceed(chain.request()) - } - - val client = DefaultClient.Builder() - .addInterceptor(customInterceptor) - .sslSocketFactory( - SSLTestUtils.clientCertificates.sslSocketFactory(), - SSLTestUtils.clientCertificates.trustManager - ) - .build() - - assertThat(client.okHttpClient.interceptors, hasSize(2)) - assertThat(client.okHttpClient.interceptors[0] is RetryInterceptor, equalTo(true)) - - enqueueMockResponse(STATUS_SUCCESS, JSON_OK) - executeRequest(HttpMethod.GET, client) - assertThat(intercepted, equalTo(true)) - } - - @Test - public fun builderShouldAddMultipleCustomInterceptors() { - val callOrder = mutableListOf() - val firstInterceptor = Interceptor { chain -> - callOrder.add("first") - chain.proceed(chain.request()) - } - val secondInterceptor = Interceptor { chain -> - callOrder.add("second") - chain.proceed(chain.request()) - } - - val client = DefaultClient.Builder() - .addInterceptor(firstInterceptor) - .addInterceptor(secondInterceptor) - .sslSocketFactory( - SSLTestUtils.clientCertificates.sslSocketFactory(), - SSLTestUtils.clientCertificates.trustManager - ) - .build() - - assertThat(client.okHttpClient.interceptors, hasSize(3)) - - enqueueMockResponse(STATUS_SUCCESS, JSON_OK) - executeRequest(HttpMethod.GET, client) - assertThat(callOrder, equalTo(listOf("first", "second"))) - } - - @Test - public fun builderShouldPlaceCustomInterceptorsBeforeLogging() { - val callOrder = mutableListOf() - val customInterceptor = Interceptor { chain -> - callOrder.add("custom") - chain.proceed(chain.request()) - } - val customLogger = HttpLoggingInterceptor.Logger { callOrder.add("logging") } - - val client = DefaultClient.Builder() - .addInterceptor(customInterceptor) - .enableLogging(true) - .logger(customLogger) - .sslSocketFactory( - SSLTestUtils.clientCertificates.sslSocketFactory(), - SSLTestUtils.clientCertificates.trustManager - ) - .build() - - assertThat(client.okHttpClient.interceptors, hasSize(3)) - assertThat(client.okHttpClient.interceptors[0] is RetryInterceptor, equalTo(true)) - assertThat(client.okHttpClient.interceptors[2] is HttpLoggingInterceptor, equalTo(true)) - - enqueueMockResponse(STATUS_SUCCESS, JSON_OK) - executeRequest(HttpMethod.GET, client) - val customIndex = callOrder.indexOf("custom") - val loggingIndex = callOrder.indexOf("logging") - assertThat(customIndex, not(equalTo(-1))) - assertThat(loggingIndex, not(equalTo(-1))) - assertThat(customIndex < loggingIndex, equalTo(true)) - } - @Test public fun builderShouldSetDefaultHeaders() { enqueueMockResponse(STATUS_SUCCESS, JSON_OK) @@ -583,12 +486,10 @@ public class DefaultClientTest { @Test public fun builderNonRetryableClientShouldInheritConfiguration() { - val customInterceptor = Interceptor { chain -> chain.proceed(chain.request()) } val client = DefaultClient.Builder() .connectTimeout(25) .readTimeout(35) .writeTimeout(45) - .addInterceptor(customInterceptor) .enableLogging(true) .build() From 90e08347223592da4fbe4ebc4cdca96d89722386 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 16 Feb 2026 11:15:19 +0530 Subject: [PATCH 17/28] Address review: remove legacy examples, logLevel, interceptors from docs --- EXAMPLES.md | 56 +------------------------------------------ V4_MIGRATION_GUIDE.md | 2 -- 2 files changed, 1 insertion(+), 57 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index b97d02f81..67b47272d 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -3223,17 +3223,6 @@ account.setNetworkingClient(netClient); ``` -
- Legacy constructor (still supported) - -```kotlin -val netClient = DefaultClient( - connectTimeout = 30, - readTimeout = 30 -) -``` -
- ### Logging configuration ```kotlin @@ -3245,12 +3234,11 @@ val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") account.networkingClient = netClient ``` -You can also customize the log level and provide a custom logger: +You can also provide a custom logger to control where logs are written: ```kotlin val netClient = DefaultClient.Builder() .enableLogging(true) - .logLevel(HttpLoggingInterceptor.Level.HEADERS) // NONE, BASIC, HEADERS, or BODY (default) .logger(HttpLoggingInterceptor.Logger { message -> Log.d("Auth0Http", message) }) .build() ``` @@ -3261,23 +3249,12 @@ val netClient = DefaultClient.Builder() ```java DefaultClient netClient = new DefaultClient.Builder() .enableLogging(true) - .logLevel(HttpLoggingInterceptor.Level.HEADERS) .build(); Auth0 account = Auth0.getInstance("client id", "domain"); account.setNetworkingClient(netClient); ``` -
- Legacy constructor (still supported) - -```kotlin -val netClient = DefaultClient( - enableLogging = true -) -``` -
- ### Set additional headers for all requests ```kotlin @@ -3304,37 +3281,6 @@ account.setNetworkingClient(netClient); ``` -
- Legacy constructor (still supported) - -```kotlin -val netClient = DefaultClient( - defaultHeaders = mapOf("{HEADER-NAME}" to "{HEADER-VALUE}") -) -``` -
- -### Custom interceptors - -You can add custom OkHttp interceptors to the `DefaultClient` for use cases such as auth token injection, analytics, or certificate pinning: - -```kotlin -val netClient = DefaultClient.Builder() - .addInterceptor(Interceptor { chain -> - val request = chain.request().newBuilder() - .addHeader("X-Request-Id", UUID.randomUUID().toString()) - .build() - chain.proceed(request) - }) - .addInterceptor(myAnalyticsInterceptor) - .build() - -val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") -account.networkingClient = netClient -``` - -Interceptors are invoked in the order they were added, after the built-in retry interceptor and before the logging interceptor. - ### Advanced configuration For more advanced configuration of the networking client, you can provide a custom implementation of `NetworkingClient`. This may be useful when you wish to reuse your own networking client, configure a proxy, etc. diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md index 97d80c09c..880c32d9b 100644 --- a/V4_MIGRATION_GUIDE.md +++ b/V4_MIGRATION_GUIDE.md @@ -120,8 +120,6 @@ val client = DefaultClient.Builder() .writeTimeout(30) .callTimeout(120) .enableLogging(true) - .logLevel(HttpLoggingInterceptor.Level.HEADERS) - .addInterceptor(myCustomInterceptor) .build() ``` From c84573b8e55746ef9a8801947b98e0bee6098946 Mon Sep 17 00:00:00 2001 From: Prince Mathew <17837162+pmathew92@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:39:23 +0530 Subject: [PATCH 18/28] breaking : Moved the `useDPoP` method in the `WebAuthProvider` class to the login builder class (#914) --- EXAMPLES.md | 4 +- V4_MIGRATION_GUIDE.md | 73 +++++++++++--- .../provider/AuthenticationActivity.kt | 2 +- .../auth0/android/provider/OAuthManager.kt | 14 ++- .../android/provider/OAuthManagerState.kt | 9 +- .../auth0/android/provider/WebAuthProvider.kt | 29 +++--- .../android/provider/OAuthManagerStateTest.kt | 84 ++++++++++++++++ .../android/provider/WebAuthProviderTest.kt | 96 +++++++++++-------- 8 files changed, 234 insertions(+), 77 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 67b47272d..047b11e5e 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -233,12 +233,12 @@ WebAuthProvider.login(account) > [!NOTE] > This feature is currently available in [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). Please reach out to Auth0 support to get it enabled for your tenant. -[DPoP](https://www.rfc-editor.org/rfc/rfc9449.html) (Demonstrating Proof of Possession) is an application-level mechanism for sender-constraining OAuth 2.0 access and refresh tokens by proving that the app is in possession of a certain private key. You can enable it by calling the `useDPoP()` method. +[DPoP](https://www.rfc-editor.org/rfc/rfc9449.html) (Demonstrating Proof of Possession) is an application-level mechanism for sender-constraining OAuth 2.0 access and refresh tokens by proving that the app is in possession of a certain private key. You can enable it by calling the `useDPoP(context)` method on the login Builder. ```kotlin WebAuthProvider - .useDPoP() .login(account) + .useDPoP(requireContext()) .start(requireContext(), object : Callback { override fun onSuccess(result: Credentials) { println("Credentials $result") diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md index 880c32d9b..28570c660 100644 --- a/V4_MIGRATION_GUIDE.md +++ b/V4_MIGRATION_GUIDE.md @@ -2,7 +2,9 @@ ## Overview -v4 of the Auth0 Android SDK includes significant build toolchain updates to support the latest Android development environment. This guide documents the changes required when migrating from v3 to v4. +v4 of the Auth0 Android SDK includes significant build toolchain updates to support the latest +Android development environment. This guide documents the changes required when migrating from v3 to +v4. ## Requirements Changes @@ -50,7 +52,8 @@ buildscript { ### Kotlin Version -v4 uses **Kotlin 2.0.21**. If you're using Kotlin in your project, you may need to update your Kotlin version to ensure compatibility. +v4 uses **Kotlin 2.0.21**. If you're using Kotlin in your project, you may need to update your +Kotlin version to ensure compatibility. ```groovy buildscript { @@ -62,20 +65,59 @@ buildscript { ### 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 +- 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 + +### DPoP Configuration Moved to Builder + +The `useDPoP(context: Context)` method has been moved from the `WebAuthProvider` object to the login +`Builder` class. This change allows DPoP to be configured per-request instead of globally. + +**v3 (global configuration — no longer supported):** + +```kotlin +// ❌ This no longer works +WebAuthProvider + .useDPoP(context) + .login(account) + .start(context, callback) +``` + +**v4 (builder-based configuration — required):** + +```kotlin +// ✅ Use this instead +WebAuthProvider + .login(account) + .useDPoP(context) + .start(context, callback) +``` + +This change ensures that DPoP configuration is scoped to individual login requests rather than +persisting across the entire application lifecycle. ## Dependency Changes ### ⚠️ Gson 2.8.9 → 2.11.0 (Transitive Dependency) -v4 updates the internal Gson dependency from **2.8.9** to **2.11.0**. While the SDK does not expose Gson types in its public API, Gson is included as a transitive runtime dependency. If your app also uses Gson, be aware of the following changes introduced in Gson 2.10+: +v4 updates the internal Gson dependency from **2.8.9** to **2.11.0**. While the SDK does not expose +Gson types in its public API, Gson is included as a transitive runtime dependency. If your app also +uses Gson, be aware of the following changes introduced in Gson 2.10+: -- **`TypeToken` with unresolved type variables is rejected at runtime.** Code like `object : TypeToken>() {}` (where `T` is a generic parameter) will throw `IllegalArgumentException`. Use Kotlin `reified` type parameters or pass concrete types instead. -- **Strict type coercion is enforced.** Gson no longer silently coerces JSON objects or arrays to `String`. If your code relies on this behavior, you will see `JsonSyntaxException`. -- **Built-in ProGuard/R8 rules are included.** Gson 2.11.0 ships its own keep rules, so you may be able to remove custom Gson ProGuard rules from your project. +- **`TypeToken` with unresolved type variables is rejected at runtime.** Code like + `object : TypeToken>() {}` (where `T` is a generic parameter) will throw + `IllegalArgumentException`. Use Kotlin `reified` type parameters or pass concrete types instead. +- **Strict type coercion is enforced.** Gson no longer silently coerces JSON objects or arrays to + `String`. If your code relies on this behavior, you will see `JsonSyntaxException`. +- **Built-in ProGuard/R8 rules are included.** Gson 2.11.0 ships its own keep rules, so you may be + able to remove custom Gson ProGuard rules from your project. If you need to pin Gson to an older version, you can use Gradle's `resolutionStrategy`: @@ -94,11 +136,14 @@ implementation('com.auth0.android:auth0:') { implementation 'com.google.code.gson:gson:2.8.9' // your preferred version ``` -> **Note:** Pinning or excluding is not recommended long-term, as the SDK has been tested and validated against Gson 2.11.0. +> **Note:** Pinning or excluding is not recommended long-term, as the SDK has been tested and +> validated against Gson 2.11.0. ### DefaultClient.Builder -v4 introduces a `DefaultClient.Builder` for configuring the HTTP client. This replaces the constructor-based approach with a more flexible builder pattern that supports additional options such as write/call timeouts, custom interceptors, and custom loggers. +v4 introduces a `DefaultClient.Builder` for configuring the HTTP client. This replaces the +constructor-based approach with a more flexible builder pattern that supports additional options +such as write/call timeouts, custom interceptors, and custom loggers. **v3 (constructor-based — deprecated):** @@ -123,7 +168,9 @@ val client = DefaultClient.Builder() .build() ``` -The legacy constructor is deprecated but **not removed** — existing code will continue to compile and run. Your IDE will show a deprecation warning with a suggested `ReplaceWith` quick-fix to migrate to the Builder. +The legacy constructor is deprecated but **not removed** — existing code will continue to compile +and run. Your IDE will show a deprecation warning with a suggested `ReplaceWith` quick-fix to +migrate to the Builder. ## Getting Help diff --git a/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.kt b/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.kt index 5f3c3f4e6..b0f413cc6 100644 --- a/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.kt +++ b/auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.kt @@ -40,7 +40,7 @@ public open class AuthenticationActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState != null) { - WebAuthProvider.onRestoreInstanceState(savedInstanceState) + WebAuthProvider.onRestoreInstanceState(savedInstanceState, this) intentLaunched = savedInstanceState.getBoolean(EXTRA_INTENT_LAUNCHED, false) } } diff --git a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt index acc2b2864..b09366e61 100644 --- a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt +++ b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt @@ -211,7 +211,8 @@ internal class OAuthManager( auth0 = account, idTokenVerificationIssuer = idTokenVerificationIssuer, idTokenVerificationLeeway = idTokenVerificationLeeway, - customAuthorizeUrl = this.customAuthorizeUrl + customAuthorizeUrl = this.customAuthorizeUrl, + dPoPEnabled = dPoP != null ) } @@ -387,14 +388,21 @@ internal class OAuthManager( internal fun OAuthManager.Companion.fromState( state: OAuthManagerState, - callback: Callback + callback: Callback, + context: Context ): OAuthManager { + // Enable DPoP on the restored PKCE's AuthenticationAPIClient so that + // the token exchange request includes the DPoP proof after process restore. + if (state.dPoPEnabled && state.pkce != null) { + state.pkce.apiClient.useDPoP(context) + } return OAuthManager( account = state.auth0, ctOptions = state.ctOptions, parameters = state.parameters, callback = callback, - customAuthorizeUrl = state.customAuthorizeUrl + customAuthorizeUrl = state.customAuthorizeUrl, + dPoP = if (state.dPoPEnabled ) DPoP(context) else null ).apply { setHeaders( state.headers diff --git a/auth0/src/main/java/com/auth0/android/provider/OAuthManagerState.kt b/auth0/src/main/java/com/auth0/android/provider/OAuthManagerState.kt index ab677af6c..06d0c6b0b 100644 --- a/auth0/src/main/java/com/auth0/android/provider/OAuthManagerState.kt +++ b/auth0/src/main/java/com/auth0/android/provider/OAuthManagerState.kt @@ -6,7 +6,6 @@ import android.util.Base64 import androidx.core.os.ParcelCompat import com.auth0.android.Auth0 import com.auth0.android.authentication.AuthenticationAPIClient -import com.auth0.android.dpop.DPoP import com.auth0.android.request.internal.GsonProvider import com.google.gson.Gson @@ -20,7 +19,7 @@ internal data class OAuthManagerState( val idTokenVerificationLeeway: Int?, val idTokenVerificationIssuer: String?, val customAuthorizeUrl: String? = null, - val dPoP: DPoP? = null + val dPoPEnabled: Boolean = false ) { private class OAuthManagerJson( @@ -37,7 +36,7 @@ internal data class OAuthManagerState( val idTokenVerificationLeeway: Int?, val idTokenVerificationIssuer: String?, val customAuthorizeUrl: String? = null, - val dPoP: DPoP? = null + val dPoPEnabled: Boolean ) fun serializeToJson( @@ -62,7 +61,7 @@ internal data class OAuthManagerState( idTokenVerificationIssuer = idTokenVerificationIssuer, idTokenVerificationLeeway = idTokenVerificationLeeway, customAuthorizeUrl = this.customAuthorizeUrl, - dPoP = this.dPoP + dPoPEnabled = this.dPoPEnabled ) return gson.toJson(json) } finally { @@ -112,7 +111,7 @@ internal data class OAuthManagerState( idTokenVerificationIssuer = oauthManagerJson.idTokenVerificationIssuer, idTokenVerificationLeeway = oauthManagerJson.idTokenVerificationLeeway, customAuthorizeUrl = oauthManagerJson.customAuthorizeUrl, - dPoP = oauthManagerJson.dPoP + dPoPEnabled = oauthManagerJson.dPoPEnabled ) } finally { parcel.recycle() diff --git a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt index 955eec9c5..758327265 100644 --- a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt +++ b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt @@ -27,10 +27,9 @@ import kotlin.coroutines.resumeWithException * * It uses an external browser by sending the [android.content.Intent.ACTION_VIEW] intent. */ -public object WebAuthProvider : SenderConstraining { +public object WebAuthProvider { private val TAG: String? = WebAuthProvider::class.simpleName private const val KEY_BUNDLE_OAUTH_MANAGER_STATE = "oauth_manager_state" - private var dPoP : DPoP? = null private val callbacks = CopyOnWriteArraySet>() @@ -49,12 +48,6 @@ public object WebAuthProvider : SenderConstraining { callbacks -= callback } - // Public methods - public override fun useDPoP(context: Context): WebAuthProvider { - dPoP = DPoP(context) - return this - } - /** * Initialize the WebAuthProvider instance for logging out the user using an account. Additional settings can be configured * in the LogoutBuilder, like changing the scheme of the return to URL. @@ -119,7 +112,7 @@ public object WebAuthProvider : SenderConstraining { } } - internal fun onRestoreInstanceState(bundle: Bundle) { + internal fun onRestoreInstanceState(bundle: Bundle, context: Context) { if (managerInstance == null) { val stateJson = bundle.getString(KEY_BUNDLE_OAUTH_MANAGER_STATE).orEmpty() if (stateJson.isNotBlank()) { @@ -138,7 +131,8 @@ public object WebAuthProvider : SenderConstraining { callback.onFailure(error) } } - } + }, + context ) } } @@ -305,7 +299,7 @@ public object WebAuthProvider : SenderConstraining { } } - public class Builder internal constructor(private val account: Auth0) { + public class Builder internal constructor(private val account: Auth0) : SenderConstraining { private val values: MutableMap = mutableMapOf() private val headers: MutableMap = mutableMapOf() private var pkce: PKCE? = null @@ -313,6 +307,7 @@ public object WebAuthProvider : SenderConstraining { private var scheme: String = "https" private var redirectUri: String? = null private var invitationUrl: String? = null + private var dPoP: DPoP? = null private var ctOptions: CustomTabsOptions = CustomTabsOptions.newBuilder().build() private var leeway: Int? = null private var launchAsTwa: Boolean = false @@ -548,6 +543,18 @@ public object WebAuthProvider : SenderConstraining { return this } + /** + * Enable DPoP (Demonstrating Proof-of-Possession) for this authentication request. + * DPoP binds access tokens to the client's cryptographic key, providing enhanced security. + * + * @param context the Android context used to access the keystore for DPoP key management + * @return the current builder instance + */ + public override fun useDPoP(context: Context): Builder { + dPoP = DPoP(context) + return this + } + /** * Request user Authentication. The result will be received in the callback. * An error is raised if there are no browser applications installed in the device, or if diff --git a/auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt b/auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt index 2cf2ec8e7..2a32f745f 100644 --- a/auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt @@ -44,4 +44,88 @@ internal class OAuthManagerStateTest { Assert.assertEquals(1, deserializedState.idTokenVerificationLeeway) Assert.assertEquals("issuer", deserializedState.idTokenVerificationIssuer) } + + @Test + fun `serialize should persist dPoPEnabled flag as true`() { + val auth0 = Auth0.getInstance("clientId", "domain") + val state = OAuthManagerState( + auth0 = auth0, + parameters = mapOf("param1" to "value1"), + headers = mapOf("header1" to "value1"), + requestCode = 1, + ctOptions = CustomTabsOptions.newBuilder() + .showTitle(true) + .withBrowserPicker( + BrowserPicker.newBuilder().withAllowedPackages(emptyList()).build() + ) + .build(), + pkce = PKCE(mock(), "redirectUri", mapOf("header1" to "value1")), + idTokenVerificationLeeway = 1, + idTokenVerificationIssuer = "issuer", + dPoPEnabled = true + ) + + val json = state.serializeToJson() + + Assert.assertTrue(json.isNotBlank()) + Assert.assertTrue(json.contains("\"dPoPEnabled\":true")) + + val deserializedState = OAuthManagerState.deserializeState(json) + + Assert.assertTrue(deserializedState.dPoPEnabled) + } + + @Test + fun `serialize should persist dPoPEnabled flag as false by default`() { + val auth0 = Auth0.getInstance("clientId", "domain") + val state = OAuthManagerState( + auth0 = auth0, + parameters = mapOf("param1" to "value1"), + headers = mapOf("header1" to "value1"), + requestCode = 1, + ctOptions = CustomTabsOptions.newBuilder() + .showTitle(true) + .withBrowserPicker( + BrowserPicker.newBuilder().withAllowedPackages(emptyList()).build() + ) + .build(), + pkce = PKCE(mock(), "redirectUri", mapOf("header1" to "value1")), + idTokenVerificationLeeway = 1, + idTokenVerificationIssuer = "issuer" + ) + + val json = state.serializeToJson() + + val deserializedState = OAuthManagerState.deserializeState(json) + + Assert.assertFalse(deserializedState.dPoPEnabled) + } + + @Test + fun `deserialize should default dPoPEnabled to false when field is missing from JSON`() { + val auth0 = Auth0.getInstance("clientId", "domain") + val state = OAuthManagerState( + auth0 = auth0, + parameters = emptyMap(), + headers = emptyMap(), + requestCode = 0, + ctOptions = CustomTabsOptions.newBuilder() + .showTitle(true) + .withBrowserPicker( + BrowserPicker.newBuilder().withAllowedPackages(emptyList()).build() + ) + .build(), + pkce = PKCE(mock(), "redirectUri", emptyMap()), + idTokenVerificationLeeway = null, + idTokenVerificationIssuer = null + ) + + val json = state.serializeToJson() + // Remove the dPoPEnabled field to simulate legacy JSON + val legacyJson = json.replace(",\"dPoPEnabled\":false", "") + + val deserializedState = OAuthManagerState.deserializeState(legacyJson) + + Assert.assertFalse(deserializedState.dPoPEnabled) + } } diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index 94baa6b3f..513d1363e 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -331,8 +331,8 @@ public class WebAuthProviderTest { @Test public fun enablingDPoPWillGenerateNewKeyPairIfOneDoesNotExist() { `when`(mockKeyStore.hasKeyPair()).thenReturn(false) - WebAuthProvider.useDPoP(mockContext) - .login(account) + login(account) + .useDPoP(mockContext) .start(activity, callback) verify(mockKeyStore).generateKeyPair(any(), any()) } @@ -358,8 +358,8 @@ public class WebAuthProviderTest { `when`(mockKeyStore.hasKeyPair()).thenReturn(true) `when`(mockKeyStore.getKeyPair()).thenReturn(Pair(mock(), FakeECPublicKey())) - WebAuthProvider.useDPoP(mockContext) - .login(account) + login(account) + .useDPoP(mockContext) .start(activity, callback) verify(activity).startActivity(intentCaptor.capture()) @@ -2741,21 +2741,13 @@ public class WebAuthProviderTest { //DPoP - public fun shouldReturnSameInstanceWhenCallingUseDPoPMultipleTimes() { - val provider1 = WebAuthProvider.useDPoP(mockContext) - val provider2 = WebAuthProvider.useDPoP(mockContext) - - assertThat(provider1, `is`(provider2)) - assertThat(WebAuthProvider.useDPoP(mockContext), `is`(provider1)) - } - @Test public fun shouldPassDPoPInstanceToOAuthManagerWhenDPoPIsEnabled() { `when`(mockKeyStore.hasKeyPair()).thenReturn(true) `when`(mockKeyStore.getKeyPair()).thenReturn(Pair(mock(), FakeECPublicKey())) - WebAuthProvider.useDPoP(mockContext) - .login(account) + login(account) + .useDPoP(mockContext) .start(activity, callback) val managerInstance = WebAuthProvider.managerInstance as OAuthManager @@ -2775,8 +2767,8 @@ public class WebAuthProviderTest { public fun shouldGenerateKeyPairWhenDPoPIsEnabledAndNoKeyPairExists() { `when`(mockKeyStore.hasKeyPair()).thenReturn(false) - WebAuthProvider.useDPoP(mockContext) - .login(account) + login(account) + .useDPoP(mockContext) .start(activity, callback) verify(mockKeyStore).generateKeyPair(any(), any()) @@ -2787,8 +2779,8 @@ public class WebAuthProviderTest { `when`(mockKeyStore.hasKeyPair()).thenReturn(true) `when`(mockKeyStore.getKeyPair()).thenReturn(Pair(mock(), FakeECPublicKey())) - WebAuthProvider.useDPoP(mockContext) - .login(account) + login(account) + .useDPoP(mockContext) .start(activity, callback) verify(mockKeyStore, never()).generateKeyPair(any(), any()) @@ -2809,8 +2801,8 @@ public class WebAuthProviderTest { `when`(mockKeyStore.hasKeyPair()).thenReturn(true) `when`(mockKeyStore.getKeyPair()).thenReturn(Pair(mock(), FakeECPublicKey())) - WebAuthProvider.useDPoP(mockContext) - .login(account) + login(account) + .useDPoP(mockContext) .start(activity, callback) verify(activity).startActivity(intentCaptor.capture()) @@ -2829,8 +2821,8 @@ public class WebAuthProviderTest { `when`(mockKeyStore.hasKeyPair()).thenReturn(true) `when`(mockKeyStore.getKeyPair()).thenReturn(null) - WebAuthProvider.useDPoP(mockContext) - .login(account) + login(account) + .useDPoP(mockContext) .start(activity, callback) verify(activity).startActivity(intentCaptor.capture()) @@ -2845,8 +2837,8 @@ public class WebAuthProviderTest { `when`(mockKeyStore.hasKeyPair()).thenReturn(true) `when`(mockKeyStore.getKeyPair()).thenReturn(Pair(mock(), FakeECPublicKey())) - val builder = WebAuthProvider.useDPoP(mockContext) - .login(account) + val builder = login(account) + .useDPoP(mockContext) .withConnection("test-connection") builder.start(activity, callback) @@ -2859,28 +2851,14 @@ public class WebAuthProviderTest { assertThat(uri, UriMatchers.hasParamWithName("dpop_jkt")) } - @Test - public fun shouldNotAffectLogoutWhenDPoPIsEnabled() { - WebAuthProvider.useDPoP(mockContext) - .logout(account) - .start(activity, voidCallback) - - verify(activity).startActivity(intentCaptor.capture()) - val uri = - intentCaptor.firstValue.getParcelableExtra(AuthenticationActivity.EXTRA_AUTHORIZE_URI) - assertThat(uri, `is`(notNullValue())) - // Logout should not have DPoP parameters - assertThat(uri, not(UriMatchers.hasParamWithName("dpop_jkt"))) - } - @Test public fun shouldHandleDPoPKeyGenerationFailureGracefully() { `when`(mockKeyStore.hasKeyPair()).thenReturn(false) doThrow(DPoPException.KEY_GENERATION_ERROR) .`when`(mockKeyStore).generateKeyPair(any(), any()) - WebAuthProvider.useDPoP(mockContext) - .login(account) + login(account) + .useDPoP(mockContext) .start(activity, callback) // Verify that the authentication fails when DPoP key generation fails @@ -2894,6 +2872,40 @@ public class WebAuthProviderTest { verify(activity, never()).startActivity(any()) } + @Test + public fun shouldNotApplyDPoPToSubsequentLoginCallsWhenNotExplicitlyEnabled() { + `when`(mockKeyStore.hasKeyPair()).thenReturn(true) + `when`(mockKeyStore.getKeyPair()).thenReturn(Pair(mock(), FakeECPublicKey())) + + // First login with DPoP enabled + login(account) + .useDPoP(mockContext) + .withScope("openid profile") + .start(activity, callback) + + verify(activity).startActivity(intentCaptor.capture()) + val firstUri = + intentCaptor.firstValue.getParcelableExtra(AuthenticationActivity.EXTRA_AUTHORIZE_URI) + assertThat(firstUri, `is`(notNullValue())) + assertThat(firstUri, UriMatchers.hasParamWithName("dpop_jkt")) + assertThat(firstUri?.getQueryParameter("scope"), `is`("openid profile")) + + // Reset the manager instance and captor for the second call + WebAuthProvider.resetManagerInstance() + + login(account) + .withScope("openid email") + .start(activity, callback) + + // Verify second startActivity call + verify(activity, times(2)).startActivity(intentCaptor.capture()) + val secondUri = + intentCaptor.lastValue.getParcelableExtra(AuthenticationActivity.EXTRA_AUTHORIZE_URI) + assertThat(secondUri, `is`(notNullValue())) + assertThat(secondUri?.getQueryParameter("scope"), `is`("openid email")) + assertThat(secondUri, not(UriMatchers.hasParamWithName("dpop_jkt"))) + } + @Test @Throws(Exception::class) public fun shouldResumeLoginSuccessfullyWithDPoPEnabled() { @@ -2909,8 +2921,8 @@ public class WebAuthProviderTest { val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) proxyAccount.networkingClient = SSLTestUtils.testClient - WebAuthProvider.useDPoP(mockContext) - .login(proxyAccount) + login(proxyAccount) + .useDPoP(mockContext) .withPKCE(pkce) .start(activity, authCallback) From 168cac48ec70966692632f23bedbbcc93d3d99d2 Mon Sep 17 00:00:00 2001 From: Prince Mathew <17837162+pmathew92@users.noreply.github.com> Date: Thu, 19 Feb 2026 08:25:34 +0530 Subject: [PATCH 19/28] feat: Add support for ephemeral session for chrome custom tabs (#916) --- EXAMPLES.md | 37 ++++ .../android/provider/CustomTabsOptions.java | 56 +++++- .../auth0/android/provider/WebAuthProvider.kt | 33 +++- .../provider/CustomTabsOptionsTest.java | 179 +++++++++++++++++- .../android/provider/WebAuthProviderTest.kt | 23 +++ 5 files changed, 321 insertions(+), 7 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 047b11e5e..49d2f181b 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -11,6 +11,7 @@ - [Changing the Return To URL scheme](#changing-the-return-to-url-scheme) - [Specify a Custom Logout URL](#specify-a-custom-logout-url) - [Trusted Web Activity](#trusted-web-activity) + - [Ephemeral Browsing [Experimental]](#ephemeral-browsing-experimental) - [DPoP [EA]](#dpop-ea) - [Authentication API](#authentication-api) - [Login with database connection](#login-with-database-connection) @@ -228,6 +229,42 @@ WebAuthProvider.login(account) .await(this) ``` +## Ephemeral Browsing [Experimental] + +> **WARNING** +> Ephemeral browsing support in Auth0.Android is still experimental and can change in the future. Please test it thoroughly in all the targeted browsers +> and OS variants and let us know your feedback. + +Ephemeral browsing launches the Chrome Custom Tab in a fully isolated session — cookies, cache, history, and credentials are deleted when the tab closes. This is equivalent to incognito/private mode for Custom Tabs, useful for privacy-focused authentication flows. + +Requires Chrome 136+ or a compatible browser. On unsupported browsers, the SDK falls back to a regular Custom Tab and logs a warning. + +```kotlin +WebAuthProvider.login(account) + .withEphemeralBrowsing() + .start(this, callback) +``` + +
+Using async/await + +```kotlin +WebAuthProvider.login(account) + .withEphemeralBrowsing() + .await(this) +``` +
+ +
+ Using Java + +```java +WebAuthProvider.login(account) + .withEphemeralBrowsing() + .start(this, callback); +``` +
+ ## DPoP [EA] > [!NOTE] diff --git a/auth0/src/main/java/com/auth0/android/provider/CustomTabsOptions.java b/auth0/src/main/java/com/auth0/android/provider/CustomTabsOptions.java index 765eed204..5df753d41 100644 --- a/auth0/src/main/java/com/auth0/android/provider/CustomTabsOptions.java +++ b/auth0/src/main/java/com/auth0/android/provider/CustomTabsOptions.java @@ -7,16 +7,19 @@ import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import android.util.Log; import androidx.annotation.ColorRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.browser.customtabs.CustomTabColorSchemeParams; +import androidx.browser.customtabs.CustomTabsClient; import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.CustomTabsSession; import androidx.browser.trusted.TrustedWebActivityIntentBuilder; import androidx.core.content.ContextCompat; +import com.auth0.android.annotation.ExperimentalAuth0Api; import com.auth0.android.authentication.AuthenticationException; import java.util.List; @@ -26,6 +29,8 @@ */ public class CustomTabsOptions implements Parcelable { + private static final String TAG = "CustomTabsOptions"; + private final boolean showTitle; @ColorRes private final int toolbarColor; @@ -34,11 +39,14 @@ public class CustomTabsOptions implements Parcelable { @Nullable private final List disabledCustomTabsPackages; - private CustomTabsOptions(boolean showTitle, @ColorRes int toolbarColor, @NonNull BrowserPicker browserPicker, @Nullable List disabledCustomTabsPackages) { + private final boolean ephemeralBrowsing; + + private CustomTabsOptions(boolean showTitle, @ColorRes int toolbarColor, @NonNull BrowserPicker browserPicker, @Nullable List disabledCustomTabsPackages, boolean ephemeralBrowsing) { this.showTitle = showTitle; this.toolbarColor = toolbarColor; this.browserPicker = browserPicker; this.disabledCustomTabsPackages = disabledCustomTabsPackages; + this.ephemeralBrowsing = ephemeralBrowsing; } @Nullable @@ -60,6 +68,12 @@ boolean isDisabledCustomTabBrowser(@NonNull String preferredPackage) { return disabledCustomTabsPackages != null && disabledCustomTabsPackages.contains(preferredPackage); } + @NonNull + CustomTabsOptions copyWithEphemeralBrowsing() { + return new CustomTabsOptions(showTitle, toolbarColor, browserPicker, + disabledCustomTabsPackages, true); + } + /** * Create a new CustomTabsOptions.Builder instance. * @@ -82,6 +96,18 @@ Intent toIntent(@NonNull Context context, @Nullable CustomTabsSession session) { final CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(session) .setShowTitle(showTitle) .setShareState(CustomTabsIntent.SHARE_STATE_OFF); + + if (ephemeralBrowsing) { + if (preferredPackage != null + && CustomTabsClient.isEphemeralBrowsingSupported(context, preferredPackage)) { + builder.setEphemeralBrowsingEnabled(true); + } else { + Log.w(TAG, "Ephemeral browsing was requested but is not supported by the " + + "current browser (" + preferredPackage + "). " + + "Falling back to a regular Custom Tab."); + } + } + if (toolbarColor > 0) { //Resource exists final CustomTabColorSchemeParams.Builder colorBuilder = new CustomTabColorSchemeParams.Builder() @@ -108,6 +134,7 @@ protected CustomTabsOptions(@NonNull Parcel in) { toolbarColor = in.readInt(); browserPicker = in.readParcelable(BrowserPicker.class.getClassLoader()); disabledCustomTabsPackages = in.createStringArrayList(); + ephemeralBrowsing = in.readByte() != 0; } @Override @@ -116,6 +143,7 @@ public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(toolbarColor); dest.writeParcelable(browserPicker, flags); dest.writeStringList(disabledCustomTabsPackages); + dest.writeByte((byte) (ephemeralBrowsing ? 1 : 0)); } @Override @@ -147,11 +175,14 @@ public static class Builder { @Nullable private List disabledCustomTabsPackages; + private boolean ephemeralBrowsing; + Builder() { this.showTitle = false; this.toolbarColor = 0; this.browserPicker = BrowserPicker.newBuilder().build(); this.disabledCustomTabsPackages = null; + this.ephemeralBrowsing = false; } /** @@ -212,6 +243,27 @@ public Builder withDisabledCustomTabsPackages(List disabledCustomTabsPac return this; } + /** + * Enable ephemeral browsing for the Custom Tab. + * When enabled, the Custom Tab runs in an isolated session — cookies, cache, + * history, and credentials are deleted when the tab closes. + * Requires Chrome 136+ or a compatible browser. On unsupported browsers, + * a warning is logged and a regular Custom Tab is used instead. + * By default, ephemeral browsing is disabled. + * + *

Warning: Ephemeral browsing support in Auth0.Android is still experimental + * and can change in the future. Please test it thoroughly in all the targeted browsers + * and OS variants and let us know your feedback.

+ * + * @return this same builder instance. + */ + @ExperimentalAuth0Api + @NonNull + public Builder withEphemeralBrowsing() { + this.ephemeralBrowsing = true; + return this; + } + /** * Create a new CustomTabsOptions instance with the customization settings. * @@ -219,7 +271,7 @@ public Builder withDisabledCustomTabsPackages(List disabledCustomTabsPac */ @NonNull public CustomTabsOptions build() { - return new CustomTabsOptions(showTitle, toolbarColor, browserPicker, disabledCustomTabsPackages); + return new CustomTabsOptions(showTitle, toolbarColor, browserPicker, disabledCustomTabsPackages, ephemeralBrowsing); } } diff --git a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt index 758327265..70cf647f3 100644 --- a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt +++ b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt @@ -7,6 +7,7 @@ import android.os.Bundle import android.util.Log import androidx.annotation.VisibleForTesting import com.auth0.android.Auth0 +import com.auth0.android.annotation.ExperimentalAuth0Api import com.auth0.android.authentication.AuthenticationException import com.auth0.android.callback.Callback import com.auth0.android.dpop.DPoP @@ -299,7 +300,8 @@ public object WebAuthProvider { } } - public class Builder internal constructor(private val account: Auth0) : SenderConstraining { + public class Builder internal constructor(private val account: Auth0) : + SenderConstraining { private val values: MutableMap = mutableMapOf() private val headers: MutableMap = mutableMapOf() private var pkce: PKCE? = null @@ -311,6 +313,7 @@ public object WebAuthProvider { private var ctOptions: CustomTabsOptions = CustomTabsOptions.newBuilder().build() private var leeway: Int? = null private var launchAsTwa: Boolean = false + private var ephemeralBrowsing: Boolean = false private var customAuthorizeUrl: String? = null /** @@ -525,6 +528,25 @@ public object WebAuthProvider { return this } + /** + * Enable ephemeral browsing for the Custom Tab used in the login flow. + * When enabled, the Custom Tab runs in an isolated session — cookies, cache, + * history, and credentials are deleted when the tab closes. + * Requires Chrome 136+ or a compatible browser. On unsupported browsers, + * a warning is logged and a regular Custom Tab is used instead. + * + * **Warning:** Ephemeral browsing support in Auth0.Android is still experimental + * and can change in the future. Please test it thoroughly in all the targeted browsers + * and OS variants and let us know your feedback. + * + * @return the current builder instance + */ + @ExperimentalAuth0Api + public fun withEphemeralBrowsing(): Builder { + ephemeralBrowsing = true + return this + } + /** * Specifies a custom Authorize URL to use for this login request, overriding the default * generated from the Auth0 domain (account.authorizeUrl). @@ -595,8 +617,15 @@ public object WebAuthProvider { values[OAuthManager.KEY_ORGANIZATION] = organizationId values[OAuthManager.KEY_INVITATION] = invitationId } + + val effectiveCtOptions = if (ephemeralBrowsing) { + ctOptions.copyWithEphemeralBrowsing() + } else { + ctOptions + } + val manager = OAuthManager( - account, callback, values, ctOptions, launchAsTwa, + account, callback, values, effectiveCtOptions, launchAsTwa, customAuthorizeUrl, dPoP ) manager.setHeaders(headers) diff --git a/auth0/src/test/java/com/auth0/android/provider/CustomTabsOptionsTest.java b/auth0/src/test/java/com/auth0/android/provider/CustomTabsOptionsTest.java index e6d86183d..939dec623 100644 --- a/auth0/src/test/java/com/auth0/android/provider/CustomTabsOptionsTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/CustomTabsOptionsTest.java @@ -5,17 +5,20 @@ import android.content.pm.PackageManager; import android.os.Parcel; +import androidx.browser.customtabs.CustomTabsClient; import androidx.browser.customtabs.CustomTabsIntent; import androidx.core.content.ContextCompat; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowLog; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -25,6 +28,7 @@ import static org.hamcrest.core.IsNull.nullValue; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -33,10 +37,20 @@ public class CustomTabsOptionsTest { private Activity context; + private MockedStatic customTabsClientMock; @Before public void setUp() { context = Robolectric.setupActivity(Activity.class); + ShadowLog.clear(); + } + + @After + public void tearDown() { + if (customTabsClientMock != null) { + customTabsClientMock.close(); + customTabsClientMock = null; + } } @Test @@ -224,4 +238,163 @@ public void shouldSetDisabledCustomTabPackages() { int resolvedColor = ContextCompat.getColor(activity, android.R.color.black); assertThat(intentWithToolbarExtra.getIntExtra(CustomTabsIntent.EXTRA_TOOLBAR_COLOR, 0), is(resolvedColor)); } -} \ No newline at end of file + + @Test + public void shouldSetEphemeralBrowsingWhenSupported() { + Activity activity = spy(Robolectric.setupActivity(Activity.class)); + BrowserPickerTest.setupBrowserContext(activity, Collections.singletonList("com.android.chrome"), null, null); + + customTabsClientMock = Mockito.mockStatic(CustomTabsClient.class); + customTabsClientMock.when(() -> + CustomTabsClient.isEphemeralBrowsingSupported(any(), eq("com.android.chrome")) + ).thenReturn(true); + + BrowserPicker browserPicker = BrowserPicker.newBuilder().build(); + CustomTabsOptions options = CustomTabsOptions.newBuilder() + .withBrowserPicker(browserPicker) + .withEphemeralBrowsing() + .build(); + assertThat(options, is(notNullValue())); + + Intent intent = options.toIntent(activity, null); + assertThat(intent, is(notNullValue())); + + // Verify ephemeral browsing extra is set on the intent + assertThat(intent.getBooleanExtra(CustomTabsIntent.EXTRA_ENABLE_EPHEMERAL_BROWSING, false), is(true)); + + // Verify isEphemeralBrowsingSupported was called + customTabsClientMock.verify(() -> + CustomTabsClient.isEphemeralBrowsingSupported(any(), eq("com.android.chrome")) + ); + + // Verify no warning was logged + assertThat(hasLogWithMessage("Ephemeral browsing was requested"), is(false)); + + // Verify Parcelable round-trip preserves the ephemeral flag + customTabsClientMock.close(); + customTabsClientMock = Mockito.mockStatic(CustomTabsClient.class); + customTabsClientMock.when(() -> + CustomTabsClient.isEphemeralBrowsingSupported(any(), eq("com.android.chrome")) + ).thenReturn(true); + + Parcel parcel = Parcel.obtain(); + options.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + CustomTabsOptions parceledOptions = CustomTabsOptions.CREATOR.createFromParcel(parcel); + assertThat(parceledOptions, is(notNullValue())); + + Intent parceledIntent = parceledOptions.toIntent(activity, null); + assertThat(parceledIntent, is(notNullValue())); + + // Verify ephemeral browsing extra is set after Parcel round-trip + assertThat(parceledIntent.getBooleanExtra(CustomTabsIntent.EXTRA_ENABLE_EPHEMERAL_BROWSING, false), is(true)); + + // Verify isEphemeralBrowsingSupported was called again after Parcel round-trip + customTabsClientMock.verify(() -> + CustomTabsClient.isEphemeralBrowsingSupported(any(), eq("com.android.chrome")) + ); + } + + @Test + public void shouldHaveEphemeralBrowsingDisabledByDefault() { + customTabsClientMock = Mockito.mockStatic(CustomTabsClient.class); + + CustomTabsOptions options = CustomTabsOptions.newBuilder().build(); + assertThat(options, is(notNullValue())); + + Intent intent = options.toIntent(context, null); + assertThat(intent, is(notNullValue())); + + customTabsClientMock.verifyNoInteractions(); + + // Verify ephemeral browsing extra is not set + assertThat(intent.getBooleanExtra(CustomTabsIntent.EXTRA_ENABLE_EPHEMERAL_BROWSING, false), is(false)); + + assertThat(hasLogWithMessage("Ephemeral browsing was requested"), is(false)); + } + + @Test + public void shouldFallbackWithWarningWhenEphemeralNotSupported() { + Activity activity = spy(Robolectric.setupActivity(Activity.class)); + BrowserPickerTest.setupBrowserContext(activity, Collections.singletonList("com.android.chrome"), null, null); + + customTabsClientMock = Mockito.mockStatic(CustomTabsClient.class); + customTabsClientMock.when(() -> + CustomTabsClient.isEphemeralBrowsingSupported(any(), eq("com.android.chrome")) + ).thenReturn(false); + + BrowserPicker browserPicker = BrowserPicker.newBuilder().build(); + CustomTabsOptions options = CustomTabsOptions.newBuilder() + .withBrowserPicker(browserPicker) + .withEphemeralBrowsing() + .build(); + + Intent intent = options.toIntent(activity, null); + assertThat(intent, is(notNullValue())); + + assertThat(hasLogWithMessage("Ephemeral browsing was requested but is not supported"), is(true)); + + // Verify ephemeral browsing extra is not set (fallback to regular Custom Tab) + assertThat(intent.getBooleanExtra(CustomTabsIntent.EXTRA_ENABLE_EPHEMERAL_BROWSING, false), is(false)); + + // Verify the intent still has standard Custom Tab extras (it's a regular Custom Tab) + assertThat(intent.hasExtra(CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE), is(true)); + } + + @Test + public void shouldFallbackWithWarningWhenPreferredPackageIsNull() { + BrowserPicker browserPicker = mock(BrowserPicker.class); + when(browserPicker.getBestBrowserPackage(any(PackageManager.class))).thenReturn(null); + + CustomTabsOptions options = CustomTabsOptions.newBuilder() + .withBrowserPicker(browserPicker) + .withEphemeralBrowsing() + .build(); + + Intent intent = options.toIntent(context, null); + assertThat(intent, is(notNullValue())); + + // Verify ephemeral browsing extra is not set (fallback to regular Custom Tab) + assertThat(intent.getBooleanExtra(CustomTabsIntent.EXTRA_ENABLE_EPHEMERAL_BROWSING, false), is(false)); + + // Verify the warning was logged with null package info + assertThat(hasLogWithMessage("Ephemeral browsing was requested but is not supported"), is(true)); + assertThat(hasLogWithMessage("(null)"), is(true)); + } + + @Test + public void shouldIgnoreEphemeralBrowsingWhenDisabledCustomTabBrowser() { + Activity activity = spy(Robolectric.setupActivity(Activity.class)); + BrowserPickerTest.setupBrowserContext(activity, Collections.singletonList("com.auth0.browser"), null, null); + BrowserPicker browserPicker = BrowserPicker.newBuilder().build(); + + CustomTabsOptions options = CustomTabsOptions.newBuilder() + .withBrowserPicker(browserPicker) + .withDisabledCustomTabsPackages(List.of("com.auth0.browser")) + .withEphemeralBrowsing() + .build(); + assertThat(options, is(notNullValue())); + + Intent intent = options.toIntent(activity, null); + + // Should return a plain ACTION_VIEW intent with no extras + assertThat(intent, is(notNullValue())); + assertThat(intent.getExtras(), is(nullValue())); + assertEquals(intent.getAction(), "android.intent.action.VIEW"); + + // No warning should be logged since the entire Custom Tab path is skipped + assertThat(hasLogWithMessage("Ephemeral browsing was requested"), is(false)); + } + + /** + * Helper to check if a log message containing the given text was emitted. + */ + private boolean hasLogWithMessage(String messageSubstring) { + for (ShadowLog.LogItem item : ShadowLog.getLogs()) { + if (item.msg != null && item.msg.contains(messageSubstring)) { + return true; + } + } + return false; + } +} diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index 513d1363e..672aeb215 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -3038,6 +3038,29 @@ public class WebAuthProviderTest { return if (hash.length == 1) "" else hash } + @Test + public fun shouldStartLoginWithEphemeralBrowsing() { + val options = Mockito.mock(CustomTabsOptions::class.java) + val ephemeralOptions = Mockito.mock(CustomTabsOptions::class.java) + `when`(options.hasCompatibleBrowser(activity.packageManager)).thenReturn(true) + `when`(options.copyWithEphemeralBrowsing()).thenReturn(ephemeralOptions) + login(account) + .withCustomTabsOptions(options) + .withEphemeralBrowsing() + .start(activity, callback) + verify(options).copyWithEphemeralBrowsing() + } + + @Test + public fun shouldNotSetEphemeralBrowsingByDefault() { + val options = Mockito.mock(CustomTabsOptions::class.java) + `when`(options.hasCompatibleBrowser(activity.packageManager)).thenReturn(true) + login(account) + .withCustomTabsOptions(options) + .start(activity, callback) + verify(options, Mockito.never()).copyWithEphemeralBrowsing() + } + private companion object { private const val KEY_STATE = "state" private const val KEY_NONCE = "nonce" From 190fdf2bc67b91945efc9189c50e01c7db93b855 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Fri, 20 Feb 2026 13:24:55 +0530 Subject: [PATCH 20/28] Add DEFAULT_MIN_TTL boundary tests for CredentialsManager and SecureCredentialsManager --- .../storage/BaseCredentialsManager.kt | 15 ++- .../storage/CredentialsManager.kt | 6 +- .../storage/SecureCredentialsManager.kt | 6 +- .../storage/SharedPreferencesStorage.kt | 4 + .../android/authentication/storage/Storage.kt | 5 + .../storage/CredentialsManagerTest.kt | 103 +++++++++++++++++- .../storage/SecureCredentialsManagerTest.kt | 100 ++++++++++++++++- .../storage/SharedPreferencesStorageTest.java | 9 ++ .../android/provider/WebAuthProviderTest.kt | 44 +++++--- .../internal/ThreadSwitcherShadow.java | 5 + 10 files changed, 268 insertions(+), 29 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index d3ac32d59..23e0c1b6a 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt @@ -22,6 +22,17 @@ public abstract class BaseCredentialsManager internal constructor( ) { private var _clock: Clock = ClockImpl() + public companion object { + /** + * Default minimum time to live (in seconds) for the access token. + * When retrieving credentials, if the access token has less than this amount of time + * remaining before expiration, it will be automatically renewed. + * This ensures the access token is valid for at least a short window after retrieval, + * preventing downstream API call failures from nearly-expired tokens. + */ + public const val DEFAULT_MIN_TTL: Int = 60 + } + /** * Updates the clock instance used for expiration verification purposes. * The use of this method can help on situations where the clock comes from an external synced source. @@ -83,7 +94,7 @@ public abstract class BaseCredentialsManager internal constructor( public abstract fun getApiCredentials( audience: String, scope: String? = null, - minTtl: Int = 0, + minTtl: Int = DEFAULT_MIN_TTL, parameters: Map = emptyMap(), headers: Map = emptyMap(), callback: Callback @@ -139,7 +150,7 @@ public abstract class BaseCredentialsManager internal constructor( public abstract suspend fun awaitApiCredentials( audience: String, scope: String? = null, - minTtl: Int = 0, + minTtl: Int = DEFAULT_MIN_TTL, parameters: Map = emptyMap(), headers: Map = emptyMap() ): APICredentials diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index 0cc5c61fa..680179670 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt @@ -224,7 +224,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting @JvmSynthetic @Throws(CredentialsManagerException::class) override suspend fun awaitCredentials(): Credentials { - return awaitCredentials(null, 0) + return awaitCredentials(null, DEFAULT_MIN_TTL) } /** @@ -370,7 +370,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting * @param callback the callback that will receive a valid [Credentials] or the [CredentialsManagerException]. */ override fun getCredentials(callback: Callback) { - getCredentials(null, 0, callback) + getCredentials(null, DEFAULT_MIN_TTL, callback) } /** @@ -682,7 +682,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting * @return whether there are valid credentials stored on this manager. */ override fun hasValidCredentials(): Boolean { - return hasValidCredentials(0) + return hasValidCredentials(DEFAULT_MIN_TTL.toLong()) } /** diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index 4a367e778..0b8279ff8 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -386,7 +386,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT @JvmSynthetic @Throws(CredentialsManagerException::class) override suspend fun awaitCredentials(): Credentials { - return awaitCredentials(null, 0) + return awaitCredentials(null, DEFAULT_MIN_TTL) } /** @@ -556,7 +556,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT override fun getCredentials( callback: Callback ) { - getCredentials(null, 0, callback) + getCredentials(null, DEFAULT_MIN_TTL, callback) } /** @@ -756,7 +756,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * @return whether this manager contains a valid non-expired pair of credentials or not. */ override fun hasValidCredentials(): Boolean { - return hasValidCredentials(0) + return hasValidCredentials(DEFAULT_MIN_TTL.toLong()) } /** diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SharedPreferencesStorage.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SharedPreferencesStorage.kt index b9c5bb8d4..72ebfebdc 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SharedPreferencesStorage.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SharedPreferencesStorage.kt @@ -75,6 +75,10 @@ public class SharedPreferencesStorage @JvmOverloads constructor( sp.edit().remove(name).apply() } + override fun removeAll() { + sp.edit().clear().apply() + } + private companion object { private const val SHARED_PREFERENCES_NAME = "com.auth0.authentication.storage" } diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/Storage.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/Storage.kt index f699cd49e..4e0644045 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/Storage.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/Storage.kt @@ -75,4 +75,9 @@ public interface Storage { * @param name the name of the value to remove. */ public fun remove(name: String) + + /** + * Removes all values from the storage. + */ + public fun removeAll() } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt index c69534fda..3423fc9de 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt @@ -3,6 +3,7 @@ package com.auth0.android.authentication.storage import com.auth0.android.NetworkErrorException import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException +import com.auth0.android.authentication.storage.BaseCredentialsManager.Companion.DEFAULT_MIN_TTL import com.auth0.android.callback.Callback import com.auth0.android.request.Request import com.auth0.android.request.internal.GsonProvider @@ -672,7 +673,7 @@ public class CredentialsManagerTest { Mockito.`when`( client.renewAuth("refresh_token", "audience") ).thenReturn(request) - val newDate = Date(CredentialsMock.CURRENT_TIME_MS + 1 * 1000) + val newDate = Date(CredentialsMock.CURRENT_TIME_MS + (DEFAULT_MIN_TTL + 10) * 1000L) val jwtMock = mock() Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) @@ -1770,6 +1771,103 @@ public class CredentialsManagerTest { MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true)) } + @Test + public fun shouldRenewCredentialsViaCallbackWhenTokenExpiresWithinDefaultMinTtl() { + // Token expires in 30 seconds, which is within DEFAULT_MIN_TTL (60s) + val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000 + Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken") + Mockito.`when`(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken") + Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") + Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("type") + Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime) + Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at")) + .thenReturn(expirationTime) + Mockito.`when`(storage.retrieveString("com.auth0.scope")).thenReturn("scope") + Mockito.`when`( + client.renewAuth("refreshToken") + ).thenReturn(request) + val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) + val jwtMock = mock() + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) + + val renewedCredentials = + Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope") + Mockito.`when`(request.execute()).thenReturn(renewedCredentials) + // Use no-arg getCredentials which now uses DEFAULT_MIN_TTL + manager.getCredentials(callback) + verify(callback).onSuccess( + credentialsCaptor.capture() + ) + // Verify renewal was triggered (client.renewAuth was called) + verify(client).renewAuth("refreshToken") + val retrievedCredentials = credentialsCaptor.firstValue + MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(retrievedCredentials.idToken, Is.`is`("newId")) + MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("newAccess")) + } + + @Test + @ExperimentalCoroutinesApi + public fun shouldAwaitRenewedCredentialsWhenTokenExpiresWithinDefaultMinTtl(): Unit = runTest { + // Token expires in 30 seconds, which is within DEFAULT_MIN_TTL (60s) + val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000 + Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken") + Mockito.`when`(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken") + Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") + Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("type") + Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime) + Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at")) + .thenReturn(expirationTime) + Mockito.`when`(storage.retrieveString("com.auth0.scope")).thenReturn("scope") + Mockito.`when`( + client.renewAuth("refreshToken") + ).thenReturn(request) + val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) + val jwtMock = mock() + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) + + val renewedCredentials = + Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope") + Mockito.`when`(request.execute()).thenReturn(renewedCredentials) + // Use no-arg awaitCredentials which now uses DEFAULT_MIN_TTL + val result = manager.awaitCredentials() + // Verify renewal was triggered + verify(client).renewAuth("refreshToken") + MatcherAssert.assertThat(result, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(result.idToken, Is.`is`("newId")) + MatcherAssert.assertThat(result.accessToken, Is.`is`("newAccess")) + } + + @Test + public fun shouldNotHaveValidCredentialsWhenTokenExpiresWithinDefaultMinTtlAndNoRefreshToken() { + // Token expires in 30 seconds, within DEFAULT_MIN_TTL (60s), and no refresh token + val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000 + Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime) + Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at")) + .thenReturn(expirationTime) + Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn(null) + Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken") + Mockito.`when`(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken") + // No-arg hasValidCredentials now uses DEFAULT_MIN_TTL, so token expiring in 30s is invalid + Assert.assertFalse(manager.hasValidCredentials()) + } + + @Test + public fun shouldHaveValidCredentialsWhenTokenExpiresWithinDefaultMinTtlButRefreshTokenAvailable() { + // Token expires in 30 seconds, within DEFAULT_MIN_TTL (60s), but refresh token is available + val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000 + Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime) + Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at")) + .thenReturn(expirationTime) + Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") + Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken") + Mockito.`when`(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken") + // Even though token expires within DEFAULT_MIN_TTL, refresh token makes it valid + MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true)) + } + @Test public fun shouldNotHaveCredentialsWhenAccessTokenAndIdTokenAreMissing() { Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn(null) @@ -1812,7 +1910,7 @@ public class CredentialsManagerTest { //now, update the clock and retry manager.setClock(object : Clock { override fun getCurrentTimeMillis(): Long { - return CredentialsMock.CURRENT_TIME_MS - 1000 + return CredentialsMock.CURRENT_TIME_MS - (DEFAULT_MIN_TTL * 1000 + 1000) } }) MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true)) @@ -1829,7 +1927,6 @@ public class CredentialsManagerTest { }) } - @Test public fun shouldAddParametersToRequest() { Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken") diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt index 45fb805b2..e6ff95f3b 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt @@ -9,6 +9,7 @@ import com.auth0.android.Auth0 import com.auth0.android.NetworkErrorException import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException +import com.auth0.android.authentication.storage.BaseCredentialsManager.Companion.DEFAULT_MIN_TTL import com.auth0.android.callback.Callback import com.auth0.android.request.Request import com.auth0.android.request.internal.GsonProvider @@ -2501,6 +2502,103 @@ public class SecureCredentialsManagerTest { MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true)) } + @Test + public fun shouldRenewCredentialsViaCallbackWhenTokenExpiresWithinDefaultMinTtl() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } + // Token expires in 30 seconds, which is within DEFAULT_MIN_TTL (60s) + val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS + 30 * 1000) + insertTestCredentials(false, true, true, expiresAt, "scope") + Mockito.`when`(storage.retrieveLong("com.auth0.credentials_access_token_expires_at")) + .thenReturn(expiresAt.time) + val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) + val jwtMock = mock() + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) + Mockito.`when`( + client.renewAuth("refreshToken") + ).thenReturn(request) + val expectedCredentials = + Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope") + Mockito.`when`(request.execute()).thenReturn(expectedCredentials) + val expectedJson = gson.toJson(expectedCredentials) + Mockito.`when`(crypto.encrypt(expectedJson.toByteArray())) + .thenReturn(expectedJson.toByteArray()) + // Use no-arg getCredentials which now uses DEFAULT_MIN_TTL + manager.getCredentials(callback) + verify(callback).onSuccess( + credentialsCaptor.capture() + ) + // Verify renewal was triggered + verify(client).renewAuth("refreshToken") + val retrievedCredentials = credentialsCaptor.firstValue + MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(retrievedCredentials.idToken, Is.`is`("newId")) + MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("newAccess")) + } + + @Test + @ExperimentalCoroutinesApi + public fun shouldAwaitRenewedCredentialsWhenTokenExpiresWithinDefaultMinTtl(): Unit = runTest { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } + // Token expires in 30 seconds, which is within DEFAULT_MIN_TTL (60s) + val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS + 30 * 1000) + insertTestCredentials(false, true, true, expiresAt, "scope") + Mockito.`when`(storage.retrieveLong("com.auth0.credentials_access_token_expires_at")) + .thenReturn(expiresAt.time) + val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) + val jwtMock = mock() + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) + Mockito.`when`( + client.renewAuth("refreshToken") + ).thenReturn(request) + val expectedCredentials = + Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope") + Mockito.`when`(request.execute()).thenReturn(expectedCredentials) + val expectedJson = gson.toJson(expectedCredentials) + Mockito.`when`(crypto.encrypt(expectedJson.toByteArray())) + .thenReturn(expectedJson.toByteArray()) + // Use no-arg awaitCredentials which now uses DEFAULT_MIN_TTL + val result = manager.awaitCredentials() + // Verify renewal was triggered + verify(client).renewAuth("refreshToken") + MatcherAssert.assertThat(result, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat(result.idToken, Is.`is`("newId")) + MatcherAssert.assertThat(result.accessToken, Is.`is`("newAccess")) + } + + @Test + public fun shouldNotHaveValidCredentialsWhenTokenExpiresWithinDefaultMinTtlAndNoRefreshToken() { + // Token expires in 30 seconds, within DEFAULT_MIN_TTL (60s), and no refresh token + val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000 + Mockito.`when`(storage.retrieveLong("com.auth0.credentials_access_token_expires_at")) + .thenReturn(expirationTime) + Mockito.`when`(storage.retrieveBoolean("com.auth0.credentials_can_refresh")) + .thenReturn(false) + Mockito.`when`(storage.retrieveString("com.auth0.credentials")) + .thenReturn("{\"access_token\":\"accessToken\"}") + // No-arg hasValidCredentials now uses DEFAULT_MIN_TTL, so token expiring in 30s is invalid + Assert.assertFalse(manager.hasValidCredentials()) + } + + @Test + public fun shouldHaveValidCredentialsWhenTokenExpiresWithinDefaultMinTtlButRefreshTokenAvailable() { + // Token expires in 30 seconds, within DEFAULT_MIN_TTL (60s), but refresh token is available + val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000 + Mockito.`when`(storage.retrieveLong("com.auth0.credentials_access_token_expires_at")) + .thenReturn(expirationTime) + Mockito.`when`(storage.retrieveBoolean("com.auth0.credentials_can_refresh")) + .thenReturn(true) + Mockito.`when`(storage.retrieveString("com.auth0.credentials")) + .thenReturn("{\"access_token\":\"accessToken\", \"refresh_token\":\"refreshToken\"}") + // Even though token expires within DEFAULT_MIN_TTL, refresh token makes it valid + MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true)) + } + @Test public fun shouldHaveCredentialsWhenTheAliasUsedHasNotBeenMigratedYet() { val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS @@ -3334,7 +3432,7 @@ public class SecureCredentialsManagerTest { //now, update the clock and retry manager.setClock(object : Clock { override fun getCurrentTimeMillis(): Long { - return CredentialsMock.CURRENT_TIME_MS - 1000 + return CredentialsMock.CURRENT_TIME_MS - (DEFAULT_MIN_TTL * 1000 + 1000) } }) MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true)) diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SharedPreferencesStorageTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/SharedPreferencesStorageTest.java index 82fdfc210..89c2c7983 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SharedPreferencesStorageTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SharedPreferencesStorageTest.java @@ -221,4 +221,13 @@ public void shouldRemovePreferencesKey() { verify(sharedPreferencesEditor).apply(); } + @Test + public void shouldRemoveAllPreferencesKeys() { + when(sharedPreferencesEditor.clear()).thenReturn(sharedPreferencesEditor); + SharedPreferencesStorage storage = new SharedPreferencesStorage(context); + storage.removeAll(); + verify(sharedPreferencesEditor).clear(); + verify(sharedPreferencesEditor).apply(); + } + } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index 672aeb215..1939fdd78 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -45,7 +45,6 @@ import org.hamcrest.core.IsNot.not import org.hamcrest.core.IsNull.notNullValue import org.junit.Assert import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers @@ -57,6 +56,7 @@ import org.mockito.MockitoAnnotations import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config +import org.robolectric.annotation.ConscryptMode import org.robolectric.shadows.ShadowLooper import java.io.ByteArrayInputStream import java.io.InputStream @@ -1539,18 +1539,17 @@ public class WebAuthProviderTest { } - // TODO: https://auth0team.atlassian.net/browse/SDK-7752 + @ConscryptMode(ConscryptMode.Mode.OFF) @Test - @Ignore("Fix these failing tests in CI once Roboelectric and other dependencies are updated") @Throws(Exception::class) public fun shouldFailToResumeLoginWhenRSAKeyIsMissingFromJWKSet() { val pkce = Mockito.mock(PKCE::class.java) `when`(pkce.codeChallenge).thenReturn("challenge") - val mockAPI = AuthenticationAPIMockServer() - mockAPI.willReturnEmptyJsonWebKeys() + val networkingClient: NetworkingClient = Mockito.spy(DefaultClient()) val authCallback = mock>() - val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) - proxyAccount.networkingClient = SSLTestUtils.testClient + val proxyAccount = + Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN) + proxyAccount.networkingClient = networkingClient login(proxyAccount) .withState("1234567890") .withNonce(JwtTestUtils.EXPECTED_NONCE) @@ -1583,12 +1582,19 @@ public class WebAuthProviderTest { Date(), "codeScope" ) + // Mock JWKS response with empty keys (no matching RSA key for kid) + val emptyJwksJson = """{"keys": []}""" + val jwksInputStream: InputStream = ByteArrayInputStream(emptyJwksJson.toByteArray()) + val jwksResponse = ServerResponse(200, jwksInputStream, emptyMap()) + Mockito.doReturn(jwksResponse).`when`(networkingClient).load( + eq(proxyAccount.getDomainUrl() + ".well-known/jwks.json"), + any() + ) Mockito.doAnswer { callbackCaptor.firstValue.onSuccess(codeCredentials) null }.`when`(pkce).getToken(eq("1234"), callbackCaptor.capture()) Assert.assertTrue(resume(intent)) - mockAPI.takeRequest() ShadowLooper.idleMainLooper() verify(authCallback).onFailure(authExceptionCaptor.capture()) val error = authExceptionCaptor.firstValue @@ -1604,7 +1610,6 @@ public class WebAuthProviderTest { error.cause?.message, `is`("Could not find a public key for kid \"key123\"") ) - mockAPI.shutdown() } @Test @@ -1674,18 +1679,17 @@ public class WebAuthProviderTest { } - //TODO: https://auth0team.atlassian.net/browse/SDK-7752 + @ConscryptMode(ConscryptMode.Mode.OFF) @Test - @Ignore("Fix these failing tests in CI once Roboelectric and other dependencies are updated") @Throws(Exception::class) public fun shouldFailToResumeLoginWhenKeyIdIsMissingFromIdTokenHeader() { val pkce = Mockito.mock(PKCE::class.java) `when`(pkce.codeChallenge).thenReturn("challenge") - val mockAPI = AuthenticationAPIMockServer() - mockAPI.willReturnValidJsonWebKeys() + val networkingClient: NetworkingClient = Mockito.spy(DefaultClient()) val authCallback = mock>() - val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain) - proxyAccount.networkingClient = SSLTestUtils.testClient + val proxyAccount = + Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN) + proxyAccount.networkingClient = networkingClient login(proxyAccount) .withState("1234567890") .withNonce("abcdefg") @@ -1717,12 +1721,19 @@ public class WebAuthProviderTest { Date(), "codeScope" ) + // Mock JWKS response with valid keys + val encoded = Files.readAllBytes(Paths.get("src/test/resources/rsa_jwks.json")) + val jwksInputStream: InputStream = ByteArrayInputStream(encoded) + val jwksResponse = ServerResponse(200, jwksInputStream, emptyMap()) + Mockito.doReturn(jwksResponse).`when`(networkingClient).load( + eq(proxyAccount.getDomainUrl() + ".well-known/jwks.json"), + any() + ) Mockito.doAnswer { callbackCaptor.firstValue.onSuccess(codeCredentials) null }.`when`(pkce).getToken(eq("1234"), callbackCaptor.capture()) Assert.assertTrue(resume(intent)) - mockAPI.takeRequest() ShadowLooper.idleMainLooper() verify(authCallback).onFailure(authExceptionCaptor.capture()) val error = authExceptionCaptor.firstValue @@ -1738,7 +1749,6 @@ public class WebAuthProviderTest { error.cause?.message, `is`("Could not find a public key for kid \"null\"") ) - mockAPI.shutdown() } @Test diff --git a/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java b/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java index 0c096ba3f..17100bf9f 100644 --- a/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java +++ b/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java @@ -26,4 +26,9 @@ public ThreadSwitcherShadow() { public void backgroundThread(Runnable runnable) { executor.execute(runnable); } + + @Implementation + public void mainThread(Runnable runnable) { + executor.execute(runnable); + } } From 56aecbc0e62aa3a96581161dcccf3c98f5fbc03f Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 23 Feb 2026 13:40:59 +0530 Subject: [PATCH 21/28] Handled review comments --- V4_MIGRATION_GUIDE.md | 82 ++++++++++++++++++- .../storage/CredentialsManager.kt | 8 +- .../storage/SecureCredentialsManager.kt | 5 +- .../android/authentication/storage/Storage.kt | 2 +- .../storage/CredentialsManagerTest.kt | 8 +- ...reCredentialsManagerBiometricPolicyTest.kt | 2 +- .../storage/SecureCredentialsManagerTest.kt | 13 +-- 7 files changed, 86 insertions(+), 34 deletions(-) diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md index 28570c660..eb015ba68 100644 --- a/V4_MIGRATION_GUIDE.md +++ b/V4_MIGRATION_GUIDE.md @@ -1,10 +1,30 @@ # Migration Guide from SDK v3 to v4 -## Overview +> **Note:** This guide is actively maintained during the v4 development phase. As new changes are merged, this document will be updated to reflect the latest breaking changes and migration steps. -v4 of the Auth0 Android SDK includes significant build toolchain updates to support the latest -Android development environment. This guide documents the changes required when migrating from v3 to -v4. +v4 of the Auth0 Android SDK includes significant build toolchain updates, updated default values for better out-of-the-box behavior, and behavior changes to simplify credential management. This guide documents the changes required when migrating from v3 to v4. + +--- + +## Table of Contents + +- [**Requirements Changes**](#requirements-changes) + + [Java Version](#java-version) + + [Gradle and Android Gradle Plugin](#gradle-and-android-gradle-plugin) + + [Kotlin Version](#kotlin-version) +- [**Breaking Changes**](#breaking-changes) + + [Classes Removed](#classes-removed) + + [DPoP Configuration Moved to Builder](#dpop-configuration-moved-to-builder) +- [**Default Values Changed**](#default-values-changed) + + [Credentials Manager minTTL](#credentials-manager-minttl) +- [**Behavior Changes**](#behavior-changes) + + [clearCredentials() Now Clears All Storage](#clearCredentials-now-clears-all-storage) + + [Storage Interface: New removeAll() Method](#storage-interface-new-removeall-method) +- [**Dependency Changes**](#dependency-changes) + + [Gson 2.8.9 → 2.11.0](#️-gson-289--2110-transitive-dependency) + + [DefaultClient.Builder](#defaultclientbuilder) + +--- ## Requirements Changes @@ -103,6 +123,60 @@ WebAuthProvider This change ensures that DPoP configuration is scoped to individual login requests rather than persisting across the entire application lifecycle. +## Default Values Changed + +### Credentials Manager `minTTL` + +**Change:** The default `minTtl` value changed from `0` to `60` seconds. + +This change affects the following Credentials Manager methods: + +- `getCredentials(callback)` / `awaitCredentials()` +- `getCredentials(scope, minTtl, callback)` / `awaitCredentials(scope, minTtl)` +- `getCredentials(scope, minTtl, parameters, callback)` / `awaitCredentials(scope, minTtl, parameters)` +- `getCredentials(scope, minTtl, parameters, forceRefresh, callback)` / `awaitCredentials(scope, minTtl, parameters, forceRefresh)` +- `getCredentials(scope, minTtl, parameters, headers, forceRefresh, callback)` / `awaitCredentials(scope, minTtl, parameters, headers, forceRefresh)` +- `hasValidCredentials()` + +**Impact:** Credentials will be renewed if they expire within 60 seconds, instead of only when already expired. + +
+ Migration example + +```kotlin +// v3 - minTtl defaulted to 0, had to be set explicitly +credentialsManager.getCredentials(scope = null, minTtl = 60, callback = callback) + +// v4 - minTtl defaults to 60 seconds +credentialsManager.getCredentials(callback) + +// v4 - use 0 to restore v3 behavior +credentialsManager.getCredentials(scope = null, minTtl = 0, callback = callback) +``` +
+ +**Reason:** A `minTtl` of `0` meant credentials were not renewed until expired, which could result in delivering access tokens that expire immediately after retrieval, causing subsequent API requests to fail. Setting a default value of `60` seconds ensures the access token remains valid for a reasonable period. + +## Behavior Changes + +### `clearCredentials()` Now Clears All Storage + +**Change:** `clearCredentials()` now calls `Storage.removeAll()` instead of removing individual credential keys. + +In v3, `clearCredentials()` removed only specific credential keys (access token, refresh token, ID token, etc.) from the underlying `Storage`. + +In v4, `clearCredentials()` calls `Storage.removeAll()`, which clears **all** values in the storage — including any API credentials stored for specific audiences. + +**Impact:** If you need to remove only the primary credentials while preserving other stored data, consider using a separate `Storage` instance for API credentials. + +**Reason:** This simplifies credential cleanup and ensures no stale data remains in storage after logout. It aligns the behavior with the Swift SDK's `clear()` method, which also clears all stored values. + +### `Storage` Interface: New `removeAll()` Method + +**Change:** The `Storage` interface now includes a `removeAll()` method with a default empty implementation. + +**Impact:** Existing custom `Storage` implementations will continue to compile and work without changes. Override `removeAll()` to provide the actual clearing behavior if your custom storage is used with `clearCredentials()`. + ## Dependency Changes ### ⚠️ Gson 2.8.9 → 2.11.0 (Transitive Dependency) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index 680179670..0d8436b4b 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt @@ -707,13 +707,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting * Removes the credentials from the storage if present. */ override fun clearCredentials() { - storage.remove(KEY_ACCESS_TOKEN) - storage.remove(KEY_REFRESH_TOKEN) - storage.remove(KEY_ID_TOKEN) - storage.remove(KEY_TOKEN_TYPE) - storage.remove(KEY_EXPIRES_AT) - storage.remove(KEY_SCOPE) - storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT) + storage.removeAll() } /** diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index 0b8279ff8..7313774b1 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -731,10 +731,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT * Delete the stored credentials */ override fun clearCredentials() { - storage.remove(KEY_CREDENTIALS) - storage.remove(KEY_EXPIRES_AT) - storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT) - storage.remove(KEY_CAN_REFRESH) + storage.removeAll() clearBiometricSession() Log.d(TAG, "Credentials were just removed from the storage") } diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/Storage.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/Storage.kt index 4e0644045..d2e788f2a 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/Storage.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/Storage.kt @@ -79,5 +79,5 @@ public interface Storage { /** * Removes all values from the storage. */ - public fun removeAll() + public fun removeAll() {} } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt index 3423fc9de..b947ba596 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt @@ -1480,13 +1480,7 @@ public class CredentialsManagerTest { @Test public fun shouldClearCredentials() { manager.clearCredentials() - verify(storage).remove("com.auth0.id_token") - verify(storage).remove("com.auth0.access_token") - verify(storage).remove("com.auth0.refresh_token") - verify(storage).remove("com.auth0.token_type") - verify(storage).remove("com.auth0.expires_at") - verify(storage).remove("com.auth0.scope") - verify(storage).remove("com.auth0.cache_expires_at") + verify(storage).removeAll() verifyNoMoreInteractions(storage) } diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerBiometricPolicyTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerBiometricPolicyTest.kt index 42449325e..d5f19678e 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerBiometricPolicyTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerBiometricPolicyTest.kt @@ -270,7 +270,7 @@ public class SecureCredentialsManagerBiometricPolicyTest { // Clear credentials manager.clearCredentials() - verify(mockStorage, atLeastOnce()).remove(any()) + verify(mockStorage).removeAll() // Session should be invalid assert(!manager.isBiometricSessionValid()) diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt index e6ff95f3b..4d29f1224 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt @@ -770,9 +770,7 @@ public class SecureCredentialsManagerTest { exception.message, Is.`is`("A change on the Lock Screen security settings have deemed the encryption keys invalid and have been recreated. Any previously stored content is now lost. Please try saving the credentials again.") ) - verify(storage).remove("com.auth0.credentials") - verify(storage).remove("com.auth0.credentials_expires_at") - verify(storage).remove("com.auth0.credentials_can_refresh") + verify(storage).removeAll() } @Test @@ -867,9 +865,7 @@ public class SecureCredentialsManagerTest { "Any previously stored content is now lost. Please try saving the credentials again." ) ) - verify(storage).remove("com.auth0.credentials") - verify(storage).remove("com.auth0.credentials_expires_at") - verify(storage).remove("com.auth0.credentials_can_refresh") + verify(storage).removeAll() } @Test @@ -2153,10 +2149,7 @@ public class SecureCredentialsManagerTest { @Test public fun shouldClearCredentials() { manager.clearCredentials() - verify(storage).remove("com.auth0.credentials") - verify(storage).remove("com.auth0.credentials_expires_at") - verify(storage).remove("com.auth0.credentials_access_token_expires_at") - verify(storage).remove("com.auth0.credentials_can_refresh") + verify(storage).removeAll() verifyNoMoreInteractions(storage) } From d2825c5d5a7f5086fcd831d8b37275205d32d8b6 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 23 Feb 2026 14:12:23 +0530 Subject: [PATCH 22/28] Fixing UT failure case --- .../java/com/auth0/android/provider/WebAuthProviderTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index 1939fdd78..f974d00b4 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -1545,7 +1545,7 @@ public class WebAuthProviderTest { public fun shouldFailToResumeLoginWhenRSAKeyIsMissingFromJWKSet() { val pkce = Mockito.mock(PKCE::class.java) `when`(pkce.codeChallenge).thenReturn("challenge") - val networkingClient: NetworkingClient = Mockito.spy(DefaultClient()) + val networkingClient: NetworkingClient = Mockito.mock(NetworkingClient::class.java) val authCallback = mock>() val proxyAccount = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN) @@ -1685,7 +1685,7 @@ public class WebAuthProviderTest { public fun shouldFailToResumeLoginWhenKeyIdIsMissingFromIdTokenHeader() { val pkce = Mockito.mock(PKCE::class.java) `when`(pkce.codeChallenge).thenReturn("challenge") - val networkingClient: NetworkingClient = Mockito.spy(DefaultClient()) + val networkingClient: NetworkingClient = Mockito.mock(NetworkingClient::class.java) val authCallback = mock>() val proxyAccount = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN) From 5163a36ca96682e234e12b97dd09eeba24dad203 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 23 Feb 2026 16:09:48 +0530 Subject: [PATCH 23/28] Removed @ConscryptMode --- .../java/com/auth0/android/provider/WebAuthProviderTest.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index f974d00b4..cbf52f501 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -56,7 +56,6 @@ import org.mockito.MockitoAnnotations import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -import org.robolectric.annotation.ConscryptMode import org.robolectric.shadows.ShadowLooper import java.io.ByteArrayInputStream import java.io.InputStream @@ -1539,7 +1538,6 @@ public class WebAuthProviderTest { } - @ConscryptMode(ConscryptMode.Mode.OFF) @Test @Throws(Exception::class) public fun shouldFailToResumeLoginWhenRSAKeyIsMissingFromJWKSet() { @@ -1679,7 +1677,6 @@ public class WebAuthProviderTest { } - @ConscryptMode(ConscryptMode.Mode.OFF) @Test @Throws(Exception::class) public fun shouldFailToResumeLoginWhenKeyIdIsMissingFromIdTokenHeader() { From 77761bdd0daf017e36c28fc24bfd3be2141c182a Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 23 Feb 2026 16:43:22 +0530 Subject: [PATCH 24/28] reverted @ignore test cases will handle in SDK-7752 --- .../java/com/auth0/android/provider/WebAuthProviderTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index cbf52f501..038708839 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -53,6 +53,7 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations +import org.junit.Ignore import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @@ -1538,6 +1539,7 @@ public class WebAuthProviderTest { } + @Ignore("Requires security provider fix - see SDK-7752") @Test @Throws(Exception::class) public fun shouldFailToResumeLoginWhenRSAKeyIsMissingFromJWKSet() { @@ -1677,6 +1679,7 @@ public class WebAuthProviderTest { } + @Ignore("Requires security provider fix - see SDK-7752") @Test @Throws(Exception::class) public fun shouldFailToResumeLoginWhenKeyIdIsMissingFromIdTokenHeader() { From 7f4ed93078af129f075cbf18400adda138df3bed Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Thu, 26 Feb 2026 10:15:36 +0530 Subject: [PATCH 25/28] Removing @ignore and running --- .../java/com/auth0/android/provider/WebAuthProviderTest.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index 038708839..cbf52f501 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -53,7 +53,6 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -import org.junit.Ignore import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @@ -1539,7 +1538,6 @@ public class WebAuthProviderTest { } - @Ignore("Requires security provider fix - see SDK-7752") @Test @Throws(Exception::class) public fun shouldFailToResumeLoginWhenRSAKeyIsMissingFromJWKSet() { @@ -1679,7 +1677,6 @@ public class WebAuthProviderTest { } - @Ignore("Requires security provider fix - see SDK-7752") @Test @Throws(Exception::class) public fun shouldFailToResumeLoginWhenKeyIdIsMissingFromIdTokenHeader() { From f4d71ee51b67eb72b4d2b518df11565dd9168fee Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Thu, 26 Feb 2026 10:47:23 +0530 Subject: [PATCH 26/28] Fix CI failure: avoid RSA key operations that crash under Conscrypt --- .../android/provider/WebAuthProviderTest.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index cbf52f501..deb1cb771 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -1555,9 +1555,11 @@ public class WebAuthProviderTest { .start(activity, authCallback) val managerInstance = WebAuthProvider.managerInstance as OAuthManager managerInstance.currentTimeInMillis = JwtTestUtils.FIXED_CLOCK_CURRENT_TIME_MS - val jwtBody = JwtTestUtils.createJWTBody() - jwtBody["iss"] = proxyAccount.getDomainUrl() - val expectedIdToken = JwtTestUtils.createTestJWT("RS256", jwtBody) + // Hardcoded RS256 JWT with kid="key123". Avoids calling JwtTestUtils.createTestJWT("RS256") + // which invokes KeyFactory.getInstance("RSA") — this crashes under Conscrypt on Linux CI. + // The JWKS mock returns empty keys, so the key lookup fails before any RSA operations. + // Header: {"alg":"RS256","typ":"JWT","kid":"key123"}, Payload: {"sub":"test"} + val expectedIdToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTEyMyJ9.eyJzdWIiOiJ0ZXN0In0.fakesignature" val intent = createAuthIntent( createHash( null, @@ -1718,9 +1720,12 @@ public class WebAuthProviderTest { Date(), "codeScope" ) - // Mock JWKS response with valid keys - val encoded = Files.readAllBytes(Paths.get("src/test/resources/rsa_jwks.json")) - val jwksInputStream: InputStream = ByteArrayInputStream(encoded) + // Use empty JWKS to avoid JwksDeserializer calling KeyFactory.getInstance("RSA") on every + // key in rsa_jwks.json — that call crashes under Conscrypt on Linux CI. + // An empty JWKS still yields PublicKeyNotFoundException(null) since no key with kid=null + // is found, which is exactly what this test asserts. + val emptyJwksJson = """{"keys": []}""" + val jwksInputStream: InputStream = ByteArrayInputStream(emptyJwksJson.toByteArray()) val jwksResponse = ServerResponse(200, jwksInputStream, emptyMap()) Mockito.doReturn(jwksResponse).`when`(networkingClient).load( eq(proxyAccount.getDomainUrl() + ".well-known/jwks.json"), From 15a20b5f4c3ee0a4e4174e7d4ff626c791bdf96c Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Thu, 26 Feb 2026 10:52:55 +0530 Subject: [PATCH 27/28] removing comments --- .../com/auth0/android/provider/WebAuthProviderTest.kt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index deb1cb771..e8bebf333 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -1555,10 +1555,6 @@ public class WebAuthProviderTest { .start(activity, authCallback) val managerInstance = WebAuthProvider.managerInstance as OAuthManager managerInstance.currentTimeInMillis = JwtTestUtils.FIXED_CLOCK_CURRENT_TIME_MS - // Hardcoded RS256 JWT with kid="key123". Avoids calling JwtTestUtils.createTestJWT("RS256") - // which invokes KeyFactory.getInstance("RSA") — this crashes under Conscrypt on Linux CI. - // The JWKS mock returns empty keys, so the key lookup fails before any RSA operations. - // Header: {"alg":"RS256","typ":"JWT","kid":"key123"}, Payload: {"sub":"test"} val expectedIdToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleTEyMyJ9.eyJzdWIiOiJ0ZXN0In0.fakesignature" val intent = createAuthIntent( createHash( @@ -1582,7 +1578,6 @@ public class WebAuthProviderTest { Date(), "codeScope" ) - // Mock JWKS response with empty keys (no matching RSA key for kid) val emptyJwksJson = """{"keys": []}""" val jwksInputStream: InputStream = ByteArrayInputStream(emptyJwksJson.toByteArray()) val jwksResponse = ServerResponse(200, jwksInputStream, emptyMap()) @@ -1720,10 +1715,6 @@ public class WebAuthProviderTest { Date(), "codeScope" ) - // Use empty JWKS to avoid JwksDeserializer calling KeyFactory.getInstance("RSA") on every - // key in rsa_jwks.json — that call crashes under Conscrypt on Linux CI. - // An empty JWKS still yields PublicKeyNotFoundException(null) since no key with kid=null - // is found, which is exactly what this test asserts. val emptyJwksJson = """{"keys": []}""" val jwksInputStream: InputStream = ByteArrayInputStream(emptyJwksJson.toByteArray()) val jwksResponse = ServerResponse(200, jwksInputStream, emptyMap()) From 812e0afc33b77e35d3fad834a0e66e4018b0a6c4 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Thu, 26 Feb 2026 11:30:53 +0530 Subject: [PATCH 28/28] Fix NPE in SignatureVerifier when JWKS key is not found --- .../java/com/auth0/android/provider/SignatureVerifier.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/auth0/src/main/java/com/auth0/android/provider/SignatureVerifier.java b/auth0/src/main/java/com/auth0/android/provider/SignatureVerifier.java index d27e5a62e..fe5bf08d4 100644 --- a/auth0/src/main/java/com/auth0/android/provider/SignatureVerifier.java +++ b/auth0/src/main/java/com/auth0/android/provider/SignatureVerifier.java @@ -57,6 +57,10 @@ static void forAsymmetricAlgorithm(@Nullable final String keyId, @NonNull Authen @Override public void onSuccess(@Nullable Map result) { PublicKey publicKey = result.get(keyId); + if (publicKey == null) { + callback.onFailure(new PublicKeyNotFoundException(keyId)); + return; + } try { callback.onSuccess(new AsymmetricSignatureVerifier(publicKey)); } catch (InvalidKeyException e) {