From 6214c03e854553463b020cc200c211cea4cf3967 Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Fri, 27 Mar 2026 16:45:23 +0530 Subject: [PATCH] Added tests cases for the DPoP validation and save scenarios --- .../storage/CredentialsManagerTest.kt | 389 ++++++++++++++++-- .../storage/SecureCredentialsManagerTest.kt | 276 ++++++++++++- 2 files changed, 623 insertions(+), 42 deletions(-) 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..42668a9ad 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 @@ -4,6 +4,10 @@ import com.auth0.android.NetworkErrorException import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException import com.auth0.android.callback.Callback +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.auth0.android.request.Request import com.auth0.android.request.internal.GsonProvider import com.auth0.android.request.internal.Jwt @@ -26,6 +30,7 @@ import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions import com.nhaarman.mockitokotlin2.verifyZeroInteractions +import com.nhaarman.mockitokotlin2.whenever import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest @@ -33,6 +38,7 @@ import org.hamcrest.MatcherAssert import org.hamcrest.Matchers import org.hamcrest.core.Is import org.hamcrest.core.IsInstanceOf +import org.junit.After import org.junit.Assert import org.junit.Assert.assertThrows import org.junit.Before @@ -74,6 +80,10 @@ public class CredentialsManagerTest { @Mock private lateinit var jwtDecoder: JWTDecoder + private lateinit var mockDPoPKeyStore: DPoPKeyStore + private val fakePublicKey = FakeECPublicKey() + private val fakePrivateKey = FakeECPrivateKey() + private val serialExecutor = Executor { runnable -> runnable.run() } private val credentialsCaptor: KArgumentCaptor = argumentCaptor() @@ -92,6 +102,10 @@ public class CredentialsManagerTest { @Before public fun setUp() { MockitoAnnotations.openMocks(this) + mockDPoPKeyStore = mock() + DPoPUtil.keyStore = mockDPoPKeyStore + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(false) + val credentialsManager = CredentialsManager(client, storage, jwtDecoder, serialExecutor) manager = Mockito.spy(credentialsManager) //Needed to test expiration verification @@ -135,6 +149,7 @@ public class CredentialsManagerTest { verify(storage).store("com.auth0.expires_at", expirationTime) verify(storage).store("com.auth0.scope", "scope") verify(storage).store("com.auth0.cache_expires_at", expirationTime) + verify(storage).remove("com.auth0.dpop_key_thumbprint") verifyNoMoreInteractions(storage) } @@ -158,6 +173,7 @@ public class CredentialsManagerTest { verify(storage).store("com.auth0.expires_at", accessTokenExpirationTime) verify(storage).store("com.auth0.scope", "scope") verify(storage).store("com.auth0.cache_expires_at", accessTokenExpirationTime) + verify(storage).remove("com.auth0.dpop_key_thumbprint") verifyNoMoreInteractions(storage) } @@ -182,6 +198,7 @@ public class CredentialsManagerTest { verify(storage).store("com.auth0.expires_at", accessTokenExpirationTime) verify(storage).store("com.auth0.scope", "scope") verify(storage).store("com.auth0.cache_expires_at", accessTokenExpirationTime) + verify(storage).remove("com.auth0.dpop_key_thumbprint") verifyNoMoreInteractions(storage) } @@ -206,6 +223,7 @@ public class CredentialsManagerTest { verify(storage).store("com.auth0.expires_at", expirationTime) verify(storage).store("com.auth0.scope", "scope") verify(storage).store("com.auth0.cache_expires_at", expirationTime) + verify(storage).remove("com.auth0.dpop_key_thumbprint") verifyNoMoreInteractions(storage) } @@ -433,7 +451,8 @@ public class CredentialsManagerTest { "token", "type", Date(accessTokenExpiry), "scope" ) - Mockito.`when`(storage.retrieveString("audience::scope")).thenReturn(gson.toJson(apiCredentials)) + Mockito.`when`(storage.retrieveString("audience::scope")) + .thenReturn(gson.toJson(apiCredentials)) manager.getApiCredentials("audience", "scope", callback = apiCredentialsCallback) verify(apiCredentialsCallback).onSuccess(apiCredentialsCaptor.capture()) val retrievedCredentials = apiCredentialsCaptor.firstValue @@ -482,7 +501,10 @@ public class CredentialsManagerTest { verify(storage).store("com.auth0.id_token", renewedCredentials.idToken) // RefreshToken should not be replaced verify(storage).store("com.auth0.refresh_token", "refreshToken") - verify(storage).store("audience::newScope", gson.toJson(renewedCredentials.toAPICredentials())) + verify(storage).store( + "audience::newScope", + gson.toJson(renewedCredentials.toAPICredentials()) + ) // Verify the returned credentials are the latest val newAPiCredentials = apiCredentialsCaptor.firstValue MatcherAssert.assertThat(newAPiCredentials, Is.`is`(Matchers.notNullValue())) @@ -500,7 +522,8 @@ public class CredentialsManagerTest { "token", "type", Date(accessTokenExpiry), "scope" ) - Mockito.`when`(storage.retrieveString("audience::scope")).thenReturn(gson.toJson(apiCredentials)) + Mockito.`when`(storage.retrieveString("audience::scope")) + .thenReturn(gson.toJson(apiCredentials)) Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") Mockito.`when`( client.renewAuth("refreshToken", "audience", "scope") @@ -542,7 +565,8 @@ public class CredentialsManagerTest { "token", "type", Date(accessTokenExpiry), "scope" ) - Mockito.`when`(storage.retrieveString("audience::scope")).thenReturn(gson.toJson(apiCredentials)) + Mockito.`when`(storage.retrieveString("audience::scope")) + .thenReturn(gson.toJson(apiCredentials)) Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") Mockito.`when`( client.renewAuth("refreshToken", "audience", "scope") @@ -557,7 +581,12 @@ public class CredentialsManagerTest { val renewedCredentials = Credentials("newId", "newAccess", "newType", newRefresh, newDate, "scope") Mockito.`when`(request.execute()).thenReturn(renewedCredentials) - manager.getApiCredentials("audience", "scope", minTtl = 10, callback = apiCredentialsCallback) + manager.getApiCredentials( + "audience", + "scope", + minTtl = 10, + callback = apiCredentialsCallback + ) verify(apiCredentialsCallback).onSuccess( apiCredentialsCaptor.capture() ) @@ -582,7 +611,7 @@ public class CredentialsManagerTest { Mockito.`when`(storage.retrieveString("audience")).thenReturn(null) Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") Mockito.`when`( - client.renewAuth("refreshToken", "audience","newScope") + client.renewAuth("refreshToken", "audience", "newScope") ).thenReturn(request) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) val jwtMock = mock() @@ -602,7 +631,10 @@ public class CredentialsManagerTest { verify(storage).store("com.auth0.id_token", renewedCredentials.idToken) // RefreshToken should be replaced verify(storage).store("com.auth0.refresh_token", "newRefreshToken") - verify(storage).store("audience::newScope", gson.toJson(renewedCredentials.toAPICredentials())) + verify(storage).store( + "audience::newScope", + gson.toJson(renewedCredentials.toAPICredentials()) + ) // Verify the returned credentials are the latest val newAPiCredentials = apiCredentialsCaptor.firstValue MatcherAssert.assertThat(newAPiCredentials, Is.`is`(Matchers.notNullValue())) @@ -630,7 +662,12 @@ public class CredentialsManagerTest { val renewedCredentials = Credentials("newId", "newAccess", "newType", newRefresh, newDate, "newScope") Mockito.`when`(request.execute()).thenReturn(renewedCredentials) - manager.getApiCredentials("audience", "newScope", minTtl = 1, callback = apiCredentialsCallback) + manager.getApiCredentials( + "audience", + "newScope", + minTtl = 1, + callback = apiCredentialsCallback + ) verify(apiCredentialsCallback).onFailure( exceptionCaptor.capture() ) @@ -651,7 +688,8 @@ public class CredentialsManagerTest { "token", "type", Date(accessTokenExpiry), "scope" ) - Mockito.`when`(storage.retrieveString("audience::scope")).thenReturn(gson.toJson(apiCredentials)) + Mockito.`when`(storage.retrieveString("audience::scope")) + .thenReturn(gson.toJson(apiCredentials)) val retrievedCredentials = manager.awaitApiCredentials("audience", "scope") MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue())) Assert.assertEquals(retrievedCredentials.accessToken, apiCredentials.accessToken) @@ -673,7 +711,7 @@ public class CredentialsManagerTest { Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) val renewedCredentials = - Credentials("newId", "newAccess", "newType",null, newDate, "newScope") + Credentials("newId", "newAccess", "newType", null, newDate, "newScope") Mockito.`when`(request.execute()).thenReturn(renewedCredentials) val retrievedCredentials = manager.awaitApiCredentials("audience") MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue())) @@ -912,7 +950,7 @@ public class CredentialsManagerTest { verify(storage).store( "com.auth0.cache_expires_at", renewedCredentials.expiresAt.time ) - verify(storage, never()).remove(ArgumentMatchers.anyString()) + verify(storage).remove("com.auth0.dpop_key_thumbprint") // Verify the returned credentials are the latest val retrievedCredentials = credentialsCaptor.firstValue @@ -970,7 +1008,7 @@ public class CredentialsManagerTest { verify(storage).store( "com.auth0.cache_expires_at", renewedCredentials.expiresAt.time ) - verify(storage, never()).remove(ArgumentMatchers.anyString()) + verify(storage).remove("com.auth0.dpop_key_thumbprint") // Verify the returned credentials are the latest val retrievedCredentials = credentialsCaptor.firstValue @@ -1027,7 +1065,7 @@ public class CredentialsManagerTest { verify(storage).store( "com.auth0.cache_expires_at", renewedCredentials.expiresAt.time ) - verify(storage, never()).remove(ArgumentMatchers.anyString()) + verify(storage).remove("com.auth0.dpop_key_thumbprint") // Verify the returned credentials are the latest val retrievedCredentials = credentialsCaptor.firstValue @@ -1086,7 +1124,7 @@ public class CredentialsManagerTest { verify(storage).store( "com.auth0.cache_expires_at", renewedCredentials.expiresAt.time ) - verify(storage, never()).remove(ArgumentMatchers.anyString()) + verify(storage).remove("com.auth0.dpop_key_thumbprint") // Verify the returned credentials are the latest val retrievedCredentials = credentialsCaptor.firstValue @@ -1144,7 +1182,7 @@ public class CredentialsManagerTest { verify(storage).store( "com.auth0.cache_expires_at", renewedCredentials.expiresAt.time ) - verify(storage, never()).remove(ArgumentMatchers.anyString()) + verify(storage).remove("com.auth0.dpop_key_thumbprint") //// Verify the returned credentials are the latest val retrievedCredentials = credentialsCaptor.firstValue @@ -1252,7 +1290,7 @@ public class CredentialsManagerTest { verify(storage).store( "com.auth0.cache_expires_at", renewedCredentials.expiresAt.time ) - verify(storage, never()).remove(ArgumentMatchers.anyString()) + verify(storage).remove("com.auth0.dpop_key_thumbprint") //// Verify the returned credentials are the latest val retrievedCredentials = credentialsCaptor.firstValue @@ -1482,6 +1520,7 @@ public class CredentialsManagerTest { verify(storage).remove("com.auth0.expires_at") verify(storage).remove("com.auth0.scope") verify(storage).remove("com.auth0.cache_expires_at") + verify(storage).remove("com.auth0.dpop_key_thumbprint") verifyNoMoreInteractions(storage) } @@ -1959,7 +1998,8 @@ public class CredentialsManagerTest { Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("type") val expirationTime = CredentialsMock.CURRENT_TIME_MS // Expired Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime) - Mockito.`when`(storage.retrieveLong("com.auth0.cache_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) @@ -1975,10 +2015,10 @@ public class CredentialsManagerTest { ) ) val mfaRequiredException = AuthenticationException(mfaRequiredValues, 403) - + MatcherAssert.assertThat(mfaRequiredException.isMultifactorRequired, Is.`is`(true)) MatcherAssert.assertThat(mfaRequiredException.getCode(), Is.`is`("mfa_required")) - + Mockito.`when`(request.execute()).thenThrow(mfaRequiredException) manager.getCredentials(callback) @@ -1988,11 +2028,20 @@ public class CredentialsManagerTest { MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) MatcherAssert.assertThat(exception.message, Matchers.containsString("authenticate")) MatcherAssert.assertThat(exception.cause, Is.`is`(mfaRequiredException)) - - MatcherAssert.assertThat(exception.mfaRequiredErrorPayload, Is.`is`(Matchers.notNullValue())) + + MatcherAssert.assertThat( + exception.mfaRequiredErrorPayload, + Is.`is`(Matchers.notNullValue()) + ) MatcherAssert.assertThat(exception.mfaToken, Is.`is`("test-mfa-token-12345")) - MatcherAssert.assertThat(exception.mfaRequiredErrorPayload?.mfaRequirements?.challenge, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(exception.mfaRequiredErrorPayload?.mfaRequirements?.challenge?.size, Is.`is`(2)) + MatcherAssert.assertThat( + exception.mfaRequiredErrorPayload?.mfaRequirements?.challenge, + Is.`is`(Matchers.notNullValue()) + ) + MatcherAssert.assertThat( + exception.mfaRequiredErrorPayload?.mfaRequirements?.challenge?.size, + Is.`is`(2) + ) } @Test @@ -2003,7 +2052,8 @@ public class CredentialsManagerTest { Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("type") val expirationTime = CredentialsMock.CURRENT_TIME_MS // Expired Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime) - Mockito.`when`(storage.retrieveLong("com.auth0.cache_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) @@ -2028,9 +2078,18 @@ public class CredentialsManagerTest { val exception = exceptionCaptor.firstValue MatcherAssert.assertThat(exception.message, Matchers.containsString("authenticate")) MatcherAssert.assertThat(exception.mfaToken, Is.`is`("enroll-mfa-token")) - MatcherAssert.assertThat(exception.mfaRequiredErrorPayload?.mfaRequirements?.enroll, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(exception.mfaRequiredErrorPayload?.mfaRequirements?.enroll?.size, Is.`is`(3)) - MatcherAssert.assertThat(exception.mfaRequiredErrorPayload?.mfaRequirements?.challenge, Is.`is`(Matchers.nullValue())) + MatcherAssert.assertThat( + exception.mfaRequiredErrorPayload?.mfaRequirements?.enroll, + Is.`is`(Matchers.notNullValue()) + ) + MatcherAssert.assertThat( + exception.mfaRequiredErrorPayload?.mfaRequirements?.enroll?.size, + Is.`is`(3) + ) + MatcherAssert.assertThat( + exception.mfaRequiredErrorPayload?.mfaRequirements?.challenge, + Is.`is`(Matchers.nullValue()) + ) } @Test @@ -2041,7 +2100,8 @@ public class CredentialsManagerTest { Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("type") val expirationTime = CredentialsMock.CURRENT_TIME_MS // Expired Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime) - Mockito.`when`(storage.retrieveLong("com.auth0.cache_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) @@ -2059,7 +2119,10 @@ public class CredentialsManagerTest { verify(callback).onFailure(exceptionCaptor.capture()) val exception = exceptionCaptor.firstValue - MatcherAssert.assertThat(exception.message, Matchers.containsString("processing the request")) + MatcherAssert.assertThat( + exception.message, + Matchers.containsString("processing the request") + ) MatcherAssert.assertThat(exception.mfaRequiredErrorPayload, Is.`is`(Matchers.nullValue())) MatcherAssert.assertThat(exception.mfaToken, Is.`is`(Matchers.nullValue())) } @@ -2073,7 +2136,8 @@ public class CredentialsManagerTest { Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("type") val expirationTime = CredentialsMock.CURRENT_TIME_MS // Expired Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime) - Mockito.`when`(storage.retrieveLong("com.auth0.cache_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) @@ -2095,7 +2159,10 @@ public class CredentialsManagerTest { } MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue())) MatcherAssert.assertThat(exception.cause, Is.`is`(mfaRequiredException)) - MatcherAssert.assertThat(exception.mfaRequiredErrorPayload, Is.`is`(Matchers.notNullValue())) + MatcherAssert.assertThat( + exception.mfaRequiredErrorPayload, + Is.`is`(Matchers.notNullValue()) + ) MatcherAssert.assertThat(exception.mfaToken, Is.`is`("await-mfa-token-12345")) } @@ -2107,7 +2174,8 @@ public class CredentialsManagerTest { Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("type") val expirationTime = CredentialsMock.CURRENT_TIME_MS // Expired Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime) - Mockito.`when`(storage.retrieveLong("com.auth0.cache_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) @@ -2123,14 +2191,263 @@ public class CredentialsManagerTest { verify(callback).onFailure(exceptionCaptor.capture()) val exception = exceptionCaptor.firstValue - + MatcherAssert.assertThat(exception.cause, Is.`is`(Matchers.notNullValue())) - MatcherAssert.assertThat(exception.cause, IsInstanceOf.instanceOf(AuthenticationException::class.java)) - + MatcherAssert.assertThat( + exception.cause, + IsInstanceOf.instanceOf(AuthenticationException::class.java) + ) + val causeException = exception.cause as AuthenticationException MatcherAssert.assertThat(causeException.getCode(), Is.`is`("mfa_required")) MatcherAssert.assertThat(causeException.isMultifactorRequired, Is.`is`(true)) - MatcherAssert.assertThat(causeException.getDescription(), Is.`is`("MFA is required for this action")) + MatcherAssert.assertThat( + causeException.getDescription(), + Is.`is`("MFA is required for this action") + ) + } + + @Test + public fun shouldStoreDPoPThumbprintWhenCredentialsTypeIsDPoP() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val credentials = CredentialsMock.create( + "idToken", "accessToken", "DPoP", "refreshToken", Date(expirationTime), "scope" + ) + prepareJwtDecoderMock(Date(expirationTime)) + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(true) + whenever(mockDPoPKeyStore.getKeyPair()).thenReturn(Pair(fakePrivateKey, fakePublicKey)) + + manager.saveCredentials(credentials) + + verify(storage).store(eq("com.auth0.dpop_key_thumbprint"), ArgumentMatchers.anyString()) + verify(storage, never()).remove("com.auth0.dpop_key_thumbprint") + } + + @Test + public fun shouldStoreDPoPThumbprintWhenIsDPoPEnabledIsTrue() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val credentials = CredentialsMock.create( + "idToken", "accessToken", "Bearer", "refreshToken", Date(expirationTime), "scope" + ) + prepareJwtDecoderMock(Date(expirationTime)) + Mockito.doReturn(true).`when`(client).isDPoPEnabled + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(true) + whenever(mockDPoPKeyStore.getKeyPair()).thenReturn(Pair(fakePrivateKey, fakePublicKey)) + + manager.saveCredentials(credentials) + + verify(storage).store(eq("com.auth0.dpop_key_thumbprint"), ArgumentMatchers.anyString()) + verify(storage, never()).remove("com.auth0.dpop_key_thumbprint") + } + + @Test + public fun shouldRemoveDPoPThumbprintForBearerCredentialsWithoutDPoP() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val credentials = CredentialsMock.create( + "idToken", "accessToken", "Bearer", "refreshToken", Date(expirationTime), "scope" + ) + prepareJwtDecoderMock(Date(expirationTime)) + + manager.saveCredentials(credentials) + + verify(storage).remove("com.auth0.dpop_key_thumbprint") + verify(storage, never()).store( + eq("com.auth0.dpop_key_thumbprint"), + ArgumentMatchers.anyString() + ) + } + + @Test + public fun shouldRemoveDPoPThumbprintWhenNoKeyPairExists() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val credentials = CredentialsMock.create( + "idToken", "accessToken", "DPoP", "refreshToken", Date(expirationTime), "scope" + ) + prepareJwtDecoderMock(Date(expirationTime)) + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(false) + + manager.saveCredentials(credentials) + + verify(storage).remove("com.auth0.dpop_key_thumbprint") + verify(storage, never()).store( + eq("com.auth0.dpop_key_thumbprint"), + ArgumentMatchers.anyString() + ) + } + + @Test + public fun shouldFailOnGetCredentialsWithDPoPKeyMissingWhenKeyNotInKeyStore() { + 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("DPoP") + val expirationTime = CredentialsMock.CURRENT_TIME_MS // expired + Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime) + Mockito.`when`(storage.retrieveString("com.auth0.scope")).thenReturn("scope") + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(false) + + manager.getCredentials(callback) + + verify(callback).onFailure(exceptionCaptor.capture()) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.DPOP_KEY_MISSING)) + } + + @Test + public fun shouldFailOnGetCredentialsWithDPoPNotConfiguredWhenClientNotSetup() { + 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("DPoP") + val expirationTime = CredentialsMock.CURRENT_TIME_MS // expired + Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime) + Mockito.`when`(storage.retrieveString("com.auth0.scope")).thenReturn("scope") + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(true) + + manager.getCredentials(callback) + + verify(callback).onFailure(exceptionCaptor.capture()) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat( + exception, + Is.`is`(CredentialsManagerException.DPOP_NOT_CONFIGURED) + ) + } + + @Test + public fun shouldFailOnGetCredentialsWithDPoPKeyMismatchWhenThumbprintsDontMatch() { + 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("DPoP") + val expirationTime = CredentialsMock.CURRENT_TIME_MS // expired + Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime) + Mockito.`when`(storage.retrieveString("com.auth0.scope")).thenReturn("scope") + Mockito.`when`(storage.retrieveString("com.auth0.dpop_key_thumbprint")) + .thenReturn("old-thumbprint-from-previous-key") + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(true) + whenever(mockDPoPKeyStore.getKeyPair()).thenReturn(Pair(fakePrivateKey, fakePublicKey)) + Mockito.doReturn(true).`when`(client).isDPoPEnabled + + manager.getCredentials(callback) + + verify(callback).onFailure(exceptionCaptor.capture()) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.DPOP_KEY_MISMATCH)) + } + + @Test + public fun shouldBackfillDPoPThumbprintForMigrationScenario() { + 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("DPoP") + val expirationTime = CredentialsMock.CURRENT_TIME_MS // expired + Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime) + Mockito.`when`(storage.retrieveString("com.auth0.scope")).thenReturn("scope") + // No stored thumbprint (migration scenario) + Mockito.`when`(storage.retrieveString("com.auth0.dpop_key_thumbprint")).thenReturn(null) + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(true) + whenever(mockDPoPKeyStore.getKeyPair()).thenReturn(Pair(fakePrivateKey, fakePublicKey)) + Mockito.doReturn(true).`when`(client).isDPoPEnabled + Mockito.`when`(client.renewAuth("refreshToken")).thenReturn(request) + val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) + val jwtMock = mock() + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) + val renewedCredentials = + Credentials("newId", "newAccess", "DPoP", "newRefresh", newDate, "scope") + Mockito.`when`(request.execute()).thenReturn(renewedCredentials) + + manager.getCredentials(callback) + + // Verify thumbprint was backfilled during validation (and also stored again during saveCredentials after renewal) + verify(storage, Mockito.atLeastOnce()).store( + eq("com.auth0.dpop_key_thumbprint"), + ArgumentMatchers.anyString() + ) + verify(callback).onSuccess(credentialsCaptor.capture()) + } + + @Test + public fun shouldTriggerDPoPValidationViaStoredThumbprintEvenForBearerTokenType() { + 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("Bearer") + val expirationTime = CredentialsMock.CURRENT_TIME_MS // expired + Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime) + Mockito.`when`(storage.retrieveString("com.auth0.scope")).thenReturn("scope") + Mockito.`when`(storage.retrieveString("com.auth0.dpop_key_thumbprint")) + .thenReturn("some-thumbprint") + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(false) + + manager.getCredentials(callback) + + verify(callback).onFailure(exceptionCaptor.capture()) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.DPOP_KEY_MISSING)) + } + + @Test + public fun shouldFailOnGetSsoCredentialsWithDPoPKeyMissingWhenKeyNotInKeyStore() { + Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") + Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("DPoP") + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(false) + + manager.getSsoCredentials(ssoCallback) + + verify(ssoCallback).onFailure(exceptionCaptor.capture()) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.DPOP_KEY_MISSING)) + } + + @Test + public fun shouldFailOnGetApiCredentialsWithDPoPKeyMissingWhenKeyNotInKeyStore() { + val accessTokenExpiry = CredentialsMock.CURRENT_TIME_MS // expired + val apiCredentials = ApiCredentialsMock.create( + accessToken = "apiToken", + type = "DPoP", + expiresAt = Date(accessTokenExpiry), + scope = "read:data" + ) + Mockito.`when`(storage.retrieveString("audience::read:data")) + .thenReturn(gson.toJson(apiCredentials)) + Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(false) + + manager.getApiCredentials("audience", "read:data", callback = apiCredentialsCallback) + + verify(apiCredentialsCallback).onFailure(exceptionCaptor.capture()) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.DPOP_KEY_MISSING)) + } + + @Test + public fun shouldUseApiCredentialTypeForDPoPValidationInsteadOfBaseTokenType() { + val accessTokenExpiry = CredentialsMock.CURRENT_TIME_MS // expired + val apiCredentials = ApiCredentialsMock.create( + accessToken = "apiToken", + type = "DPoP", + expiresAt = Date(accessTokenExpiry), + scope = "read:data" + ) + Mockito.`when`(storage.retrieveString("audience::read:data")) + .thenReturn(gson.toJson(apiCredentials)) + Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken") + Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("Bearer") + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(false) + + manager.getApiCredentials("audience", "read:data", callback = apiCredentialsCallback) + + verify(apiCredentialsCallback).onFailure(exceptionCaptor.capture()) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.DPOP_KEY_MISSING)) + } + + @After + public fun tearDown() { + DPoPUtil.keyStore = DPoPKeyStore() } private fun prepareJwtDecoderMock(expiresAt: Date?) { 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..10d495030 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 @@ -10,6 +10,10 @@ import com.auth0.android.NetworkErrorException import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException import com.auth0.android.callback.Callback +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.auth0.android.request.Request import com.auth0.android.request.internal.GsonProvider import com.auth0.android.request.internal.Jwt @@ -31,6 +35,7 @@ import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions +import com.nhaarman.mockitokotlin2.whenever import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest @@ -38,6 +43,7 @@ import org.hamcrest.MatcherAssert import org.hamcrest.Matchers import org.hamcrest.core.Is import org.hamcrest.core.IsInstanceOf +import org.junit.After import org.junit.Assert import org.junit.Assert.assertThrows import org.junit.Before @@ -103,6 +109,10 @@ public class SecureCredentialsManagerTest { private lateinit var fragmentActivity: FragmentActivity + private lateinit var mockDPoPKeyStore: DPoPKeyStore + private val fakePublicKey = FakeECPublicKey() + private val fakePrivateKey = FakeECPrivateKey() + private val serialExecutor = Executor { runnable -> runnable.run() } private val credentialsCaptor: KArgumentCaptor = argumentCaptor() @@ -122,6 +132,9 @@ public class SecureCredentialsManagerTest { @Before public fun setUp() { MockitoAnnotations.openMocks(this) + mockDPoPKeyStore = mock() + DPoPUtil.keyStore = mockDPoPKeyStore + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(false) val activity = Robolectric.buildActivity(Activity::class.java).create().start().resume().get() val activityContext = Mockito.spy(activity) @@ -612,6 +625,8 @@ public class SecureCredentialsManagerTest { verify(storage) .store("com.auth0.credentials_access_token_expires_at", sharedExpirationTime) verify(storage).store("com.auth0.credentials_can_refresh", true) + verify(storage).store("com.auth0.token_type", "type") + verify(storage).remove("com.auth0.dpop_key_thumbprint") verifyNoMoreInteractions(storage) val encodedJson = stringCaptor.firstValue MatcherAssert.assertThat(encodedJson, Is.`is`(Matchers.notNullValue())) @@ -647,6 +662,8 @@ public class SecureCredentialsManagerTest { verify(storage) .store("com.auth0.credentials_access_token_expires_at", accessTokenExpirationTime) verify(storage).store("com.auth0.credentials_can_refresh", true) + verify(storage).store("com.auth0.token_type", "type") + verify(storage).remove("com.auth0.dpop_key_thumbprint") verifyNoMoreInteractions(storage) val encodedJson = stringCaptor.firstValue MatcherAssert.assertThat(encodedJson, Is.`is`(Matchers.notNullValue())) @@ -686,6 +703,8 @@ public class SecureCredentialsManagerTest { verify(storage) .store("com.auth0.credentials_access_token_expires_at", accessTokenExpirationTime) verify(storage).store("com.auth0.credentials_can_refresh", true) + verify(storage).store("com.auth0.token_type", "type") + verify(storage).remove("com.auth0.dpop_key_thumbprint") verifyNoMoreInteractions(storage) val encodedJson = stringCaptor.firstValue MatcherAssert.assertThat(encodedJson, Is.`is`(Matchers.notNullValue())) @@ -725,6 +744,8 @@ public class SecureCredentialsManagerTest { verify(storage) .store("com.auth0.credentials_access_token_expires_at", expirationTime) verify(storage).store("com.auth0.credentials_can_refresh", false) + verify(storage).store("com.auth0.token_type", "type") + verify(storage).remove("com.auth0.dpop_key_thumbprint") verifyNoMoreInteractions(storage) val encodedJson = stringCaptor.firstValue MatcherAssert.assertThat(encodedJson, Is.`is`(Matchers.notNullValue())) @@ -1245,7 +1266,7 @@ public class SecureCredentialsManagerTest { verify(storage).store("com.auth0.credentials_expires_at", newDate.time) verify(storage).store("com.auth0.credentials_access_token_expires_at", newDate.time) verify(storage).store("com.auth0.credentials_can_refresh", true) - verify(storage, never()).remove(anyString()) + verify(storage).remove("com.auth0.dpop_key_thumbprint") // Verify the returned credentials are the latest val retrievedCredentials = credentialsCaptor.firstValue @@ -1369,7 +1390,7 @@ public class SecureCredentialsManagerTest { verify(storage).store("com.auth0.credentials_expires_at", newDate.time) verify(storage).store("com.auth0.credentials_access_token_expires_at", newDate.time) verify(storage).store("com.auth0.credentials_can_refresh", true) - verify(storage, never()).remove(anyString()) + verify(storage).remove("com.auth0.dpop_key_thumbprint") // Verify the returned credentials are the latest val retrievedCredentials = credentialsCaptor.firstValue @@ -1437,7 +1458,7 @@ public class SecureCredentialsManagerTest { verify(storage).store("com.auth0.credentials_expires_at", newDate.time) verify(storage).store("com.auth0.credentials_access_token_expires_at", newDate.time) verify(storage).store("com.auth0.credentials_can_refresh", true) - verify(storage, never()).remove(anyString()) + verify(storage).remove("com.auth0.dpop_key_thumbprint") // Verify the returned credentials are the latest val retrievedCredentials = credentialsCaptor.firstValue @@ -1507,7 +1528,7 @@ public class SecureCredentialsManagerTest { verify(storage).store("com.auth0.credentials_expires_at", newDate.time) verify(storage).store("com.auth0.credentials_access_token_expires_at", newDate.time) verify(storage).store("com.auth0.credentials_can_refresh", true) - verify(storage, never()).remove(anyString()) + verify(storage).remove("com.auth0.dpop_key_thumbprint") // Verify the returned credentials are the latest val retrievedCredentials = credentialsCaptor.firstValue @@ -1592,7 +1613,7 @@ public class SecureCredentialsManagerTest { .store(eq("com.auth0.credentials"), stringCaptor.capture()) verify(storage).store("com.auth0.credentials_expires_at", newDate.time) verify(storage).store("com.auth0.credentials_can_refresh", true) - verify(storage, never()).remove(anyString()) + verify(storage).remove("com.auth0.dpop_key_thumbprint") // Verify the returned credentials are the latest val retrievedCredentials = credentialsCaptor.firstValue @@ -1656,7 +1677,7 @@ public class SecureCredentialsManagerTest { .store(eq("com.auth0.credentials"), stringCaptor.capture()) verify(storage).store("com.auth0.credentials_expires_at", newDate.time) verify(storage).store("com.auth0.credentials_can_refresh", true) - verify(storage, never()).remove(anyString()) + verify(storage).remove("com.auth0.dpop_key_thumbprint") // Verify the returned credentials are the latest val retrievedCredentials = credentialsCaptor.firstValue @@ -2153,6 +2174,8 @@ public class SecureCredentialsManagerTest { 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).remove("com.auth0.token_type") + verify(storage).remove("com.auth0.dpop_key_thumbprint") verifyNoMoreInteractions(storage) } @@ -3598,6 +3621,247 @@ public class SecureCredentialsManagerTest { } + @Test + public fun shouldStoreDPoPThumbprintWhenCredentialsTypeIsDPoP() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val credentials = CredentialsMock.create( + "idToken", "accessToken", "DPoP", "refreshToken", Date(expirationTime), "scope" + ) + val json = gson.toJson(credentials) + prepareJwtDecoderMock(Date(expirationTime)) + Mockito.`when`(crypto.encrypt(json.toByteArray())).thenReturn(json.toByteArray()) + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(true) + whenever(mockDPoPKeyStore.getKeyPair()).thenReturn(Pair(fakePrivateKey, fakePublicKey)) + + manager.saveCredentials(credentials) + + verify(storage).store(eq("com.auth0.dpop_key_thumbprint"), anyString()) + verify(storage, never()).remove("com.auth0.dpop_key_thumbprint") + } + + @Test + public fun shouldStoreDPoPThumbprintWhenIsDPoPEnabledIsTrue() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val credentials = CredentialsMock.create( + "idToken", "accessToken", "Bearer", "refreshToken", Date(expirationTime), "scope" + ) + val json = gson.toJson(credentials) + prepareJwtDecoderMock(Date(expirationTime)) + Mockito.`when`(crypto.encrypt(json.toByteArray())).thenReturn(json.toByteArray()) + Mockito.doReturn(true).`when`(client).isDPoPEnabled + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(true) + whenever(mockDPoPKeyStore.getKeyPair()).thenReturn(Pair(fakePrivateKey, fakePublicKey)) + + manager.saveCredentials(credentials) + + verify(storage).store(eq("com.auth0.dpop_key_thumbprint"), anyString()) + verify(storage, never()).remove("com.auth0.dpop_key_thumbprint") + } + + @Test + public fun shouldRemoveDPoPThumbprintForBearerCredentialsWithoutDPoP() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val credentials = CredentialsMock.create( + "idToken", "accessToken", "Bearer", "refreshToken", Date(expirationTime), "scope" + ) + val json = gson.toJson(credentials) + prepareJwtDecoderMock(Date(expirationTime)) + Mockito.`when`(crypto.encrypt(json.toByteArray())).thenReturn(json.toByteArray()) + + manager.saveCredentials(credentials) + + verify(storage).remove("com.auth0.dpop_key_thumbprint") + verify(storage, never()).store(eq("com.auth0.dpop_key_thumbprint"), anyString()) + } + + @Test + public fun shouldRemoveDPoPThumbprintWhenNoKeyPairExists() { + val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS + val credentials = CredentialsMock.create( + "idToken", "accessToken", "DPoP", "refreshToken", Date(expirationTime), "scope" + ) + val json = gson.toJson(credentials) + prepareJwtDecoderMock(Date(expirationTime)) + Mockito.`when`(crypto.encrypt(json.toByteArray())).thenReturn(json.toByteArray()) + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(false) + + manager.saveCredentials(credentials) + + verify(storage).remove("com.auth0.dpop_key_thumbprint") + verify(storage, never()).store(eq("com.auth0.dpop_key_thumbprint"), anyString()) + } + + + @Test + public fun shouldFailOnGetCredentialsWithDPoPKeyMissingWhenKeyNotInKeyStore() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } + val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS) // expired + insertTestCredentials(true, true, true, expiresAt, "scope") + Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("DPoP") + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(false) + + manager.getCredentials(callback) + + verify(callback).onFailure(exceptionCaptor.capture()) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.DPOP_KEY_MISSING)) + } + + @Test + public fun shouldFailOnGetCredentialsWithDPoPNotConfiguredWhenClientNotSetup() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } + val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS) // expired + insertTestCredentials(true, true, true, expiresAt, "scope") + Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("DPoP") + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(true) + + manager.getCredentials(callback) + + verify(callback).onFailure(exceptionCaptor.capture()) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.DPOP_NOT_CONFIGURED)) + } + + @Test + public fun shouldFailOnGetCredentialsWithDPoPKeyMismatchWhenThumbprintsDontMatch() { + Mockito.`when`(localAuthenticationManager.authenticate()).then { + localAuthenticationManager.resultCallback.onSuccess(true) + } + val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS) // expired + insertTestCredentials(true, true, true, expiresAt, "scope") + Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("DPoP") + Mockito.`when`(storage.retrieveString("com.auth0.dpop_key_thumbprint")) + .thenReturn("old-thumbprint-from-previous-key") + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(true) + whenever(mockDPoPKeyStore.getKeyPair()).thenReturn(Pair(fakePrivateKey, fakePublicKey)) + Mockito.doReturn(true).`when`(client).isDPoPEnabled + + manager.getCredentials(callback) + + verify(callback).onFailure(exceptionCaptor.capture()) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.DPOP_KEY_MISMATCH)) + } + + @Test + public fun shouldBackfillDPoPThumbprintForMigrationScenario() { + val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS) // expired + insertTestCredentials(true, true, true, expiresAt, "scope") + Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("DPoP") + // No stored thumbprint (migration scenario) + Mockito.`when`(storage.retrieveString("com.auth0.dpop_key_thumbprint")).thenReturn(null) + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(true) + whenever(mockDPoPKeyStore.getKeyPair()).thenReturn(Pair(fakePrivateKey, fakePublicKey)) + Mockito.doReturn(true).`when`(client).isDPoPEnabled + Mockito.`when`(client.renewAuth("refreshToken")).thenReturn(request) + val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) + val jwtMock = mock() + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) + val renewedCredentials = + Credentials("newId", "newAccess", "DPoP", "newRefresh", newDate, "scope") + Mockito.`when`(request.execute()).thenReturn(renewedCredentials) + val expectedJson = gson.toJson(renewedCredentials) + Mockito.`when`(crypto.encrypt(any())).thenReturn(expectedJson.toByteArray()) + + manager.continueGetCredentials(null, 0, emptyMap(), emptyMap(), false, callback) + + // Verify thumbprint was backfilled during validation (and also stored again during saveCredentials after renewal) + verify(storage, Mockito.atLeastOnce()).store(eq("com.auth0.dpop_key_thumbprint"), anyString()) + verify(callback).onSuccess(credentialsCaptor.capture()) + } + + @Test + public fun shouldTriggerDPoPValidationViaStoredThumbprintEvenForBearerTokenType() { + val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS) // expired + insertTestCredentials(true, true, true, expiresAt, "scope") + Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("Bearer") + // Thumbprint exists (DPoP was used, but token_type is Bearer — RFC 9449 edge case) + Mockito.`when`(storage.retrieveString("com.auth0.dpop_key_thumbprint")) + .thenReturn("some-thumbprint") + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(false) + + manager.continueGetCredentials(null, 0, emptyMap(), emptyMap(), false, callback) + + verify(callback).onFailure(exceptionCaptor.capture()) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.DPOP_KEY_MISSING)) + } + + @Test + public fun shouldFailOnGetSsoCredentialsWithDPoPKeyMissingWhenKeyNotInKeyStore() { + val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) + insertTestCredentials(true, true, true, expiresAt, "scope") + Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("DPoP") + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(false) + + manager.getSsoCredentials(ssoCallback) + + verify(ssoCallback).onFailure(exceptionCaptor.capture()) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.DPOP_KEY_MISSING)) + } + + @Test + public fun shouldFailOnGetApiCredentialsWithDPoPKeyMissingWhenKeyNotInKeyStore() { + val accessTokenExpiry = CredentialsMock.CURRENT_TIME_MS // expired + val apiCredentials = ApiCredentialsMock.create( + accessToken = "apiToken", + type = "DPoP", + expiresAt = Date(accessTokenExpiry), + scope = "read:data" + ) + val storedJson = gson.toJson(apiCredentials) + val encoded = String(Base64.encode(storedJson.toByteArray(), Base64.DEFAULT)) + Mockito.`when`(crypto.decrypt(storedJson.toByteArray())) + .thenReturn(storedJson.toByteArray()) + Mockito.`when`(storage.retrieveString("audience::read:data")).thenReturn(encoded) + val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) + insertTestCredentials(true, true, true, expiresAt, "scope") + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(false) + + manager.continueGetApiCredentials("audience", "read:data", 0, emptyMap(), emptyMap(), apiCredentialsCallback) + + verify(apiCredentialsCallback).onFailure(exceptionCaptor.capture()) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.DPOP_KEY_MISSING)) + } + + @Test + public fun shouldUseApiCredentialTypeForDPoPValidationInsteadOfBaseTokenType() { + val accessTokenExpiry = CredentialsMock.CURRENT_TIME_MS // expired + val apiCredentials = ApiCredentialsMock.create( + accessToken = "apiToken", + type = "DPoP", + expiresAt = Date(accessTokenExpiry), + scope = "read:data" + ) + val storedJson = gson.toJson(apiCredentials) + val encoded = String(Base64.encode(storedJson.toByteArray(), Base64.DEFAULT)) + Mockito.`when`(crypto.decrypt(storedJson.toByteArray())) + .thenReturn(storedJson.toByteArray()) + Mockito.`when`(storage.retrieveString("audience::read:data")).thenReturn(encoded) + Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("Bearer") + val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) + insertTestCredentials(true, true, true, expiresAt, "scope") + whenever(mockDPoPKeyStore.hasKeyPair()).thenReturn(false) + + manager.continueGetApiCredentials("audience", "read:data", 0, emptyMap(), emptyMap(), apiCredentialsCallback) + + verify(apiCredentialsCallback).onFailure(exceptionCaptor.capture()) + val exception = exceptionCaptor.firstValue + MatcherAssert.assertThat(exception, Is.`is`(CredentialsManagerException.DPOP_KEY_MISSING)) + } + + @After + public fun tearDown() { + DPoPUtil.keyStore = DPoPKeyStore() + } + private fun prepareJwtDecoderMock(expiresAt: Date?) { val jwtMock = mock() Mockito.`when`(jwtMock.expiresAt).thenReturn(expiresAt)