Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file removed .github/workflows/rl-scanner.yml
Empty file.
1 change: 0 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ on:
pull_request:
branches:
- main
- v4_development
push:
branches:
- main
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,7 @@ gen-external-apklibs
.TemporaryItems
.Trashes

version.txt
version.txt

plans/
docs/
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@

### Requirements

Android API version 31 or later and Java 17+.
Android API version 26 or later and Java 17+.

> :warning: Applications targeting Android SDK version 30 (`targetSdkVersion = 30`) and below should use version 2.9.0.
> :warning: Applications with `minSdkVersion` below 26 should continue using the v3 line of the SDK.

Here’s what you need in `build.gradle` to target Java 17 bytecode for Android and Kotlin plugins respectively.

Expand Down Expand Up @@ -120,7 +120,7 @@ android {
compileSdkVersion 36
defaultConfig {
applicationId "com.auth0.samples"
minSdkVersion 24
minSdkVersion 26
targetSdkVersion 36
//...

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@ public class MyAccountAPIClient @VisibleForTesting(otherwise = VisibleForTesting
/**
* Retrieves a detailed list of authentication methods belonging to the user.
*
* ## Scopes Required
*
* `read:me:authentication_methods`
*
* ## Usage
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ private void launchAsAuthTab(@NonNull Context context, @NonNull Uri uri, @NonNul
builder.setSession(authSession);
}
AuthTabIntent authTabIntent = builder.build();
authTabIntent.launch(authTabLauncher, uri, scheme);
threadSwitcher.mainThread(() -> authTabIntent.launch(authTabLauncher, uri, scheme));
}

private void launchAsDefault(Context context, Uri uri) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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.dpop.DPoPException
import com.auth0.android.dpop.DPoPKeyStore
import com.auth0.android.dpop.DPoPUtil
import com.auth0.android.dpop.FakeECPrivateKey
Expand Down Expand Up @@ -116,6 +117,15 @@ public class SecureCredentialsManagerTest {

private val serialExecutor = Executor { runnable -> runnable.run() }

// Executors spun up by the concurrency tests, shut down in tearDown() so no
// non-daemon threads leak and cause the suite to hang or flake afterwards.
private val managedExecutors = mutableListOf<ExecutorService>()

private fun <T : ExecutorService> track(executor: T): T {
managedExecutors.add(executor)
return executor
}

private val credentialsCaptor: KArgumentCaptor<Credentials> = argumentCaptor()

private val exceptionCaptor: KArgumentCaptor<CredentialsManagerException> = argumentCaptor()
Expand Down Expand Up @@ -2122,8 +2132,8 @@ public class SecureCredentialsManagerTest {
Mockito.`when`(client.renewAuth(refreshToken = "refreshToken")).thenReturn(request)
Mockito.`when`(request.execute()).thenReturn(renewedCredentials)

val sharedExecutor = Executors.newSingleThreadExecutor()
val callerPool: ExecutorService = Executors.newFixedThreadPool(3)
val sharedExecutor = track(Executors.newSingleThreadExecutor())
val callerPool: ExecutorService = track(Executors.newFixedThreadPool(3))
val latch = CountDownLatch(3)
val context: Context =
Robolectric.buildActivity(Activity::class.java).create().start().resume().get()
Expand Down Expand Up @@ -2191,8 +2201,8 @@ public class SecureCredentialsManagerTest {
Mockito.`when`(client.renewAuth(refreshToken = "refreshToken")).thenReturn(request)
Mockito.`when`(request.execute()).thenReturn(renewedCredentials)

val singleThreadExecutor = Executors.newSingleThreadExecutor()
val callerPool = Executors.newFixedThreadPool(3)
val singleThreadExecutor = track(Executors.newSingleThreadExecutor())
val callerPool = track(Executors.newFixedThreadPool(3))
val latch = CountDownLatch(3)
val context: Context =
Robolectric.buildActivity(Activity::class.java).create().start().resume().get()
Expand Down Expand Up @@ -2247,8 +2257,8 @@ public class SecureCredentialsManagerTest {
"scope"
)

val singleThreadExecutor = Executors.newSingleThreadExecutor()
val callerPool = Executors.newFixedThreadPool(3)
val singleThreadExecutor = track(Executors.newSingleThreadExecutor())
val callerPool = track(Executors.newFixedThreadPool(3))
val latch = CountDownLatch(3)
val context: Context =
Robolectric.buildActivity(Activity::class.java).create().start().resume().get()
Expand Down Expand Up @@ -2309,8 +2319,8 @@ public class SecureCredentialsManagerTest {
Mockito.`when`(client.renewAuth(refreshToken = "refreshToken")).thenReturn(request)
Mockito.`when`(request.execute()).thenReturn(renewedCredentials)

val singleThreadExecutor = Executors.newSingleThreadExecutor()
val callerPool = Executors.newFixedThreadPool(2)
val singleThreadExecutor = track(Executors.newSingleThreadExecutor())
val callerPool = track(Executors.newFixedThreadPool(2))
val latch = CountDownLatch(2)
val context: Context =
Robolectric.buildActivity(Activity::class.java).create().start().resume().get()
Expand Down Expand Up @@ -2380,6 +2390,22 @@ public class SecureCredentialsManagerTest {
manager.clearAll()
verify(storage).removeAll()
verify(crypto).deleteAllKeys()
verify(mockDPoPKeyStore).deleteKeyPair()
Assert.assertFalse(manager.isBiometricSessionValid())
}

@Test
public fun shouldClearStorageAndKeysEvenWhenDPoPKeyDeletionFails() {
whenever(mockDPoPKeyStore.deleteKeyPair())
.thenThrow(DPoPException.KEY_STORE_ERROR)

// clearAll swallows DPoP deletion errors, so it must not throw and the
// storage / crypto / biometric cleanup must still complete.
manager.clearAll()

verify(storage).removeAll()
verify(crypto).deleteAllKeys()
verify(mockDPoPKeyStore).deleteKeyPair()
Assert.assertFalse(manager.isBiometricSessionValid())
}

Expand Down Expand Up @@ -4455,7 +4481,11 @@ public class SecureCredentialsManagerTest {
val renewedCredentials =
Credentials("newId", "newAccess", "newType", null, newDate, "newScope")
Mockito.`when`(request.execute()).thenReturn(renewedCredentials)
// First encryption (base credentials save) succeeds; the second
// (renewed API credentials save) throws, so the failure under test is
// the API-credential persistence path, not the preceding base save.
Mockito.`when`(crypto.encrypt(any()))
.thenAnswer { it.arguments[0] as ByteArray }
.thenThrow(CryptoException("CryptoException is thrown"))

manager.continueGetApiCredentials(
Expand All @@ -4475,6 +4505,8 @@ public class SecureCredentialsManagerTest {
@After
public fun tearDown() {
DPoPUtil.keyStore = DPoPKeyStore()
managedExecutors.forEach { it.shutdownNow() }
managedExecutors.clear()
}

private fun prepareJwtDecoderMock(expiresAt: Date?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package com.auth0.android.request.internal
import com.auth0.android.result.CredentialsMock
import com.auth0.android.result.SSOCredentials
import com.google.gson.Gson
import com.google.gson.JsonParseException
import org.hamcrest.CoreMatchers
import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers
import org.hamcrest.core.Is
import org.junit.Assert
import org.junit.Before
import org.junit.Test

Expand Down Expand Up @@ -63,6 +65,50 @@ public class SSOCredentialsDeserializerTest {
MatcherAssert.assertThat(credentials.refreshToken, Is.`is`(CoreMatchers.nullValue()))
}

@Test
@Throws(Exception::class)
public fun shouldThrowWhenJsonIsNotObject() {
val exception = Assert.assertThrows(JsonParseException::class.java) {
gson.getAdapter(SSOCredentials::class.java).fromJson("[]")
}
MatcherAssert.assertThat(
exception.message,
Is.`is`("sso credentials json is not a valid json object")
)
}

@Test
@Throws(Exception::class)
public fun shouldThrowWhenJsonObjectIsEmpty() {
val exception = Assert.assertThrows(JsonParseException::class.java) {
gson.getAdapter(SSOCredentials::class.java).fromJson("{}")
}
MatcherAssert.assertThat(
exception.message,
Is.`is`("sso credentials json is not a valid json object")
)
}

@Test
@Throws(Exception::class)
public fun shouldThrowWhenExpiresInIsMissing() {
val json = """
{
"access_token": "session-transfer-token",
"id_token": "id-token-value",
"issued_token_type": "urn:auth0:params:oauth:token-type:session-transfer-token",
"token_type": "N_A"
}
""".trimIndent()
val exception = Assert.assertThrows(JsonParseException::class.java) {
gson.getAdapter(SSOCredentials::class.java).fromJson(json)
}
MatcherAssert.assertThat(
exception.message,
Is.`is`("Missing the required property expires_in")
)
}

private fun generateSSOCredentialsJSON(): String {
return """
{
Expand Down
Loading