diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md index eb015ba6..f96d03cb 100644 --- a/V4_MIGRATION_GUIDE.md +++ b/V4_MIGRATION_GUIDE.md @@ -15,6 +15,7 @@ v4 of the Auth0 Android SDK includes significant build toolchain updates, update - [**Breaking Changes**](#breaking-changes) + [Classes Removed](#classes-removed) + [DPoP Configuration Moved to Builder](#dpop-configuration-moved-to-builder) + + [SSOCredentials.expiresIn Renamed to expiresAt](#ssocredentialsexpiresin-renamed-to-expiresat) - [**Default Values Changed**](#default-values-changed) + [Credentials Manager minTTL](#credentials-manager-minttl) - [**Behavior Changes**](#behavior-changes) @@ -123,6 +124,28 @@ WebAuthProvider This change ensures that DPoP configuration is scoped to individual login requests rather than persisting across the entire application lifecycle. +### `SSOCredentials.expiresIn` Renamed to `expiresAt` + +**Change:** The `expiresIn` property in `SSOCredentials` has been renamed to `expiresAt` and its type changed from `Int` to `Date`. + +In v3, `expiresIn` held the raw number of seconds until the session transfer token expired. In v4, the SDK now automatically converts this value into an absolute expiration `Date` (computed as current time + seconds) during deserialization, consistent with how `Credentials.expiresAt` works. The property has been renamed to `expiresAt` to reflect that it now represents an absolute point in time rather than a duration. + +**v3:** + +```kotlin +val ssoCredentials: SSOCredentials = // ... +val secondsUntilExpiry: Int = ssoCredentials.expiresIn +``` + +**v4:** + +```kotlin +val ssoCredentials: SSOCredentials = // ... +val expirationDate: Date = ssoCredentials.expiresAt +``` + +**Impact:** If your code references `ssoCredentials.expiresIn`, rename it to `ssoCredentials.expiresAt`. The value is now an absolute `Date` instead of a duration in seconds. + ## Default Values Changed ### Credentials Manager `minTTL` diff --git a/auth0/src/main/java/com/auth0/android/request/internal/GsonProvider.kt b/auth0/src/main/java/com/auth0/android/request/internal/GsonProvider.kt index 9157f5ba..a927a89d 100755 --- a/auth0/src/main/java/com/auth0/android/request/internal/GsonProvider.kt +++ b/auth0/src/main/java/com/auth0/android/request/internal/GsonProvider.kt @@ -2,6 +2,7 @@ package com.auth0.android.request.internal import androidx.annotation.VisibleForTesting import com.auth0.android.result.Credentials +import com.auth0.android.result.SSOCredentials import com.auth0.android.result.UserProfile import com.google.gson.Gson import com.google.gson.GsonBuilder @@ -25,6 +26,7 @@ internal object GsonProvider { .registerTypeAdapterFactory(JsonRequiredTypeAdapterFactory()) .registerTypeAdapter(UserProfile::class.java, UserProfileDeserializer()) .registerTypeAdapter(Credentials::class.java, CredentialsDeserializer()) + .registerTypeAdapter(SSOCredentials::class.java, SSOCredentialsDeserializer()) .registerTypeAdapter(jwksType, JwksDeserializer()) .setDateFormat(DATE_FORMAT) .create() diff --git a/auth0/src/main/java/com/auth0/android/request/internal/SSOCredentialsDeserializer.kt b/auth0/src/main/java/com/auth0/android/request/internal/SSOCredentialsDeserializer.kt new file mode 100644 index 00000000..bcc5f657 --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/request/internal/SSOCredentialsDeserializer.kt @@ -0,0 +1,70 @@ +package com.auth0.android.request.internal + +import androidx.annotation.VisibleForTesting +import com.auth0.android.result.SSOCredentials +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import java.lang.reflect.Type +import java.util.Date + +internal open class SSOCredentialsDeserializer : JsonDeserializer { + @Throws(JsonParseException::class) + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): SSOCredentials { + if (!json.isJsonObject || json.isJsonNull || json.asJsonObject.entrySet().isEmpty()) { + throw JsonParseException("sso credentials json is not a valid json object") + } + val jsonObject = json.asJsonObject + val sessionTransferToken = + context.deserialize(jsonObject.remove("access_token"), String::class.java) + val idToken = + context.deserialize(jsonObject.remove("id_token"), String::class.java) + val issuedTokenType = + context.deserialize(jsonObject.remove("issued_token_type"), String::class.java) + val tokenType = + context.deserialize(jsonObject.remove("token_type"), String::class.java) + val expiresIn = + context.deserialize(jsonObject.remove("expires_in"), Long::class.java) + val refreshToken = + context.deserialize(jsonObject.remove("refresh_token"), String::class.java) + + var expiresInDate: Date? + if (expiresIn != null) { + expiresInDate = Date(currentTimeInMillis + expiresIn * 1000) + } else { + throw JsonParseException("Missing the required property expires_in") + } + + return createSSOCredentials( + sessionTransferToken, + idToken, + issuedTokenType, + tokenType, + expiresInDate!!, + refreshToken + ) + } + + @get:VisibleForTesting + open val currentTimeInMillis: Long + get() = System.currentTimeMillis() + + @VisibleForTesting + open fun createSSOCredentials( + sessionTransferToken: String, + idToken: String, + issuedTokenType: String, + tokenType: String, + expiresAt: Date, + refreshToken: String? + ): SSOCredentials { + return SSOCredentials( + sessionTransferToken, idToken, issuedTokenType, tokenType, expiresAt, refreshToken + ) + } +} diff --git a/auth0/src/main/java/com/auth0/android/result/SSOCredentials.kt b/auth0/src/main/java/com/auth0/android/result/SSOCredentials.kt index f5681208..dec78883 100644 --- a/auth0/src/main/java/com/auth0/android/result/SSOCredentials.kt +++ b/auth0/src/main/java/com/auth0/android/result/SSOCredentials.kt @@ -1,6 +1,7 @@ package com.auth0.android.result import com.google.gson.annotations.SerializedName +import java.util.Date /** * Holds the token credentials required for web SSO. @@ -46,12 +47,12 @@ public data class SSOCredentials( @field:SerializedName("token_type") public val tokenType: String, /** - * Expiration duration of the session transfer token in seconds. Session transfer tokens are short-lived and expire after a few minutes. + * Expiration date of the session transfer token. Session transfer tokens are short-lived and expire after a few minutes. * Once expired, the session transfer tokens can no longer be used for web SSO. * - * @return the expiration duration of this session transfer token + * @return the expiration Date of this session transfer token */ - @field:SerializedName("expires_in") public val expiresIn: Int, + @field:SerializedName("expires_in") public val expiresAt: Date, /** * Rotated refresh token. Only available when Refresh Token Rotation is enabled. @@ -67,6 +68,6 @@ public data class SSOCredentials( ) { override fun toString(): String { - return "SSOCredentials(sessionTransferToken = ****, idToken = ****,issuedTokenType = $issuedTokenType, tokenType = $tokenType, expiresIn = $expiresIn, refreshToken = ****)" + return "SSOCredentials(sessionTransferToken = ****, idToken = ****,issuedTokenType = $issuedTokenType, tokenType = $tokenType, expiresAt = $expiresAt, refreshToken = ****)" } } \ No newline at end of file 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 350ec5d2..cf281942 100755 --- a/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt @@ -2381,7 +2381,7 @@ public class AuthenticationAPIClientTest { @Test public fun shouldSsoExchange() { - mockAPI.willReturnSuccessfulLogin() + mockAPI.willReturnSuccessfulSSOExchange() val callback = MockAuthenticationCallback() client.ssoExchange("refresh-token") .start(callback) @@ -2413,7 +2413,7 @@ public class AuthenticationAPIClientTest { @Test public fun shouldSsoExchangeSync() { - mockAPI.willReturnSuccessfulLogin() + mockAPI.willReturnSuccessfulSSOExchange() val sessionTransferCredentials = client.ssoExchange("refresh-token") .execute() val request = mockAPI.takeRequest() @@ -2437,7 +2437,7 @@ public class AuthenticationAPIClientTest { @Test @ExperimentalCoroutinesApi public fun shouldAwaitSsoExchange(): Unit = runTest { - mockAPI.willReturnSuccessfulLogin() + mockAPI.willReturnSuccessfulSSOExchange() val ssoCredentials = client .ssoExchange("refresh-token") .await() @@ -3096,7 +3096,7 @@ public class AuthenticationAPIClientTest { whenever(mockKeyStore.hasKeyPair()).thenReturn(true) whenever(mockKeyStore.getKeyPair()).thenReturn(Pair(FakeECPrivateKey(), FakeECPublicKey())) - mockAPI.willReturnSuccessfulLogin() + mockAPI.willReturnSuccessfulSSOExchange() val callback = MockAuthenticationCallback() client.useDPoP(mockContext).ssoExchange("refresh-token") 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 b947ba59..7a8ef667 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 @@ -260,7 +260,7 @@ public class CredentialsManagerTest { verifyNoMoreInteractions(storage) val ssoCredentials = SSOCredentialsMock.create( "accessToken", "identityToken", - "issuedTokenType", "tokenType", null, 60 + "issuedTokenType", "tokenType", null, Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000) ) manager.saveSsoCredentials(ssoCredentials) } @@ -270,7 +270,7 @@ public class CredentialsManagerTest { verifyNoMoreInteractions(storage) val ssoCredentials = SSOCredentialsMock.create( "accessToken", "identityToken", - "issuedTokenType", "tokenType", "refresh_token", 60 + "issuedTokenType", "tokenType", "refresh_token", Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000) ) Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")) .thenReturn("refresh_token") @@ -283,7 +283,7 @@ public class CredentialsManagerTest { verifyNoMoreInteractions(storage) val ssoCredentials = SSOCredentialsMock.create( "accessToken", "identityToken", - "issuedTokenType", "tokenType", "refresh_token", 60 + "issuedTokenType", "tokenType", "refresh_token", Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000) ) Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")) .thenReturn("refresh-token") @@ -313,6 +313,7 @@ public class CredentialsManagerTest { .thenReturn("refresh_token_old") Mockito.`when`(client.ssoExchange("refresh_token_old")) .thenReturn(SSOCredentialsRequest) + val ssoExpiresAt = Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000) Mockito.`when`(SSOCredentialsRequest.execute()).thenReturn( SSOCredentialsMock.create( "web-sso-token", @@ -320,7 +321,7 @@ public class CredentialsManagerTest { "issued-token-type", "token-type", "refresh-token", - 60 + ssoExpiresAt ) ) manager.getSsoCredentials(ssoCallback) @@ -333,7 +334,7 @@ public class CredentialsManagerTest { MatcherAssert.assertThat(credentials.tokenType, Is.`is`("token-type")) MatcherAssert.assertThat(credentials.issuedTokenType, Is.`is`("issued-token-type")) MatcherAssert.assertThat(credentials.refreshToken, Is.`is`("refresh-token")) - MatcherAssert.assertThat(credentials.expiresIn, Is.`is`(60)) + MatcherAssert.assertThat(credentials.expiresAt, Is.`is`(ssoExpiresAt)) verify(storage).store("com.auth0.refresh_token", credentials.refreshToken) } @@ -409,6 +410,7 @@ public class CredentialsManagerTest { .thenReturn("refresh_token_old") Mockito.`when`(client.ssoExchange("refresh_token_old")) .thenReturn(SSOCredentialsRequest) + val ssoExpiresAt = Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000) Mockito.`when`(SSOCredentialsRequest.execute()).thenReturn( SSOCredentialsMock.create( "web-sso-token", @@ -416,7 +418,7 @@ public class CredentialsManagerTest { "issued-token-type", "token-type", "refresh-token", - 60 + ssoExpiresAt ) ) val credentials = manager.awaitSsoCredentials() @@ -425,7 +427,7 @@ public class CredentialsManagerTest { MatcherAssert.assertThat(credentials.tokenType, Is.`is`("token-type")) MatcherAssert.assertThat(credentials.issuedTokenType, Is.`is`("issued-token-type")) MatcherAssert.assertThat(credentials.refreshToken, Is.`is`("refresh-token")) - MatcherAssert.assertThat(credentials.expiresIn, Is.`is`(60)) + MatcherAssert.assertThat(credentials.expiresAt, Is.`is`(ssoExpiresAt)) verify(storage).store("com.auth0.refresh_token", credentials.refreshToken) } 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 4d29f122..ef8ba7bd 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 @@ -187,7 +187,7 @@ public class SecureCredentialsManagerTest { verifyNoMoreInteractions(storage) val ssoCredentials = SSOCredentialsMock.create( "accessToken", "identityToken", - "issuedTokenType", "tokenType", "refresh_token", 60 + "issuedTokenType", "tokenType", "refresh_token", Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000) ) val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) val storedJson = insertTestCredentials( @@ -209,7 +209,7 @@ public class SecureCredentialsManagerTest { verifyNoMoreInteractions(storage) val sessionTransferCredentials = SSOCredentialsMock.create( "accessToken", "identityToken", - "issuedTokenType", "tokenType", "refresh_token", 60 + "issuedTokenType", "tokenType", "refresh_token", Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000) ) val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) insertTestCredentials( @@ -310,6 +310,7 @@ public class SecureCredentialsManagerTest { val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) Mockito.`when`(client.ssoExchange("refreshToken")) .thenReturn(SSOCredentialsRequest) + val ssoExpiresAt = Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000) Mockito.`when`(SSOCredentialsRequest.execute()).thenReturn( SSOCredentialsMock.create( "web-sso-token", @@ -317,7 +318,7 @@ public class SecureCredentialsManagerTest { "issued-token-type", "token-type", "refresh-token", - 60 + ssoExpiresAt ) ) insertTestCredentials( @@ -347,7 +348,7 @@ public class SecureCredentialsManagerTest { MatcherAssert.assertThat(credentials.tokenType, Is.`is`("token-type")) MatcherAssert.assertThat(credentials.issuedTokenType, Is.`is`("issued-token-type")) MatcherAssert.assertThat(credentials.refreshToken, Is.`is`("refresh-token")) - MatcherAssert.assertThat(credentials.expiresIn, Is.`is`(60)) + MatcherAssert.assertThat(credentials.expiresAt, Is.`is`(ssoExpiresAt)) verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture()) val encodedJson = stringCaptor.firstValue MatcherAssert.assertThat(encodedJson, Is.`is`(Matchers.notNullValue())) @@ -491,6 +492,7 @@ public class SecureCredentialsManagerTest { val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) Mockito.`when`(client.ssoExchange("refreshToken")) .thenReturn(SSOCredentialsRequest) + val ssoExpiresAt = Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000) Mockito.`when`(SSOCredentialsRequest.execute()).thenReturn( SSOCredentialsMock.create( "web-sso-token", @@ -498,7 +500,7 @@ public class SecureCredentialsManagerTest { "issued-token-type", "token-type", "refresh-token", - 60 + ssoExpiresAt ) ) insertTestCredentials( @@ -526,7 +528,7 @@ public class SecureCredentialsManagerTest { MatcherAssert.assertThat(credentials.tokenType, Is.`is`("token-type")) MatcherAssert.assertThat(credentials.issuedTokenType, Is.`is`("issued-token-type")) MatcherAssert.assertThat(credentials.refreshToken, Is.`is`("refresh-token")) - MatcherAssert.assertThat(credentials.expiresIn, Is.`is`(60)) + MatcherAssert.assertThat(credentials.expiresAt, Is.`is`(ssoExpiresAt)) verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture()) val encodedJson = stringCaptor.firstValue MatcherAssert.assertThat(encodedJson, Is.`is`(Matchers.notNullValue())) diff --git a/auth0/src/test/java/com/auth0/android/request/internal/SSOCredentialsDeserializerMock.kt b/auth0/src/test/java/com/auth0/android/request/internal/SSOCredentialsDeserializerMock.kt new file mode 100644 index 00000000..48e4f961 --- /dev/null +++ b/auth0/src/test/java/com/auth0/android/request/internal/SSOCredentialsDeserializerMock.kt @@ -0,0 +1,24 @@ +package com.auth0.android.request.internal + +import com.auth0.android.result.CredentialsMock +import com.auth0.android.result.SSOCredentials +import com.auth0.android.result.SSOCredentialsMock +import java.util.* + +internal class SSOCredentialsDeserializerMock : SSOCredentialsDeserializer() { + override fun createSSOCredentials( + sessionTransferToken: String, + idToken: String, + issuedTokenType: String, + tokenType: String, + expiresAt: Date, + refreshToken: String? + ): SSOCredentials { + return SSOCredentialsMock.create( + sessionTransferToken, idToken, issuedTokenType, tokenType, refreshToken, expiresAt + ) + } + + override val currentTimeInMillis: Long + get() = CredentialsMock.CURRENT_TIME_MS +} diff --git a/auth0/src/test/java/com/auth0/android/request/internal/SSOCredentialsDeserializerTest.kt b/auth0/src/test/java/com/auth0/android/request/internal/SSOCredentialsDeserializerTest.kt new file mode 100644 index 00000000..31a5aa5b --- /dev/null +++ b/auth0/src/test/java/com/auth0/android/request/internal/SSOCredentialsDeserializerTest.kt @@ -0,0 +1,90 @@ +package com.auth0.android.request.internal + +import com.auth0.android.result.CredentialsMock +import com.auth0.android.result.SSOCredentials +import com.google.gson.Gson +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.hamcrest.core.Is +import org.junit.Before +import org.junit.Test + +public class SSOCredentialsDeserializerTest { + private lateinit var gson: Gson + + @Before + public fun setUp() { + val deserializer = SSOCredentialsDeserializerMock() + gson = GsonProvider.gson.newBuilder() + .registerTypeAdapter(SSOCredentials::class.java, deserializer) + .create() + } + + @Test + @Throws(Exception::class) + public fun shouldSetExpiresInFromExpiresInSeconds() { + val json = generateSSOCredentialsJSON() + val credentials = gson.getAdapter(SSOCredentials::class.java).fromJson(json) + MatcherAssert.assertThat(credentials.expiresAt, Is.`is`(CoreMatchers.notNullValue())) + val expiresAt = credentials.expiresAt.time.toDouble() + val expectedExpiresAt = (CredentialsMock.CURRENT_TIME_MS + 300 * 1000).toDouble() + MatcherAssert.assertThat(expiresAt, Is.`is`(Matchers.closeTo(expectedExpiresAt, 1.0))) + } + + @Test + @Throws(Exception::class) + public fun shouldDeserializeAllFields() { + val json = generateSSOCredentialsJSON() + val credentials = gson.getAdapter(SSOCredentials::class.java).fromJson(json) + MatcherAssert.assertThat( + credentials.sessionTransferToken, + Is.`is`("session-transfer-token") + ) + MatcherAssert.assertThat(credentials.idToken, Is.`is`("id-token-value")) + MatcherAssert.assertThat( + credentials.issuedTokenType, + Is.`is`("urn:auth0:params:oauth:token-type:session-transfer-token") + ) + MatcherAssert.assertThat(credentials.tokenType, Is.`is`("N_A")) + MatcherAssert.assertThat(credentials.refreshToken, Is.`is`("refresh-token-value")) + } + + @Test + @Throws(Exception::class) + public fun shouldDeserializeWithNullRefreshToken() { + val json = generateSSOCredentialsJSONWithoutRefreshToken() + val credentials = gson.getAdapter(SSOCredentials::class.java).fromJson(json) + MatcherAssert.assertThat( + credentials.sessionTransferToken, + Is.`is`("session-transfer-token") + ) + MatcherAssert.assertThat(credentials.idToken, Is.`is`("id-token-value")) + MatcherAssert.assertThat(credentials.refreshToken, Is.`is`(CoreMatchers.nullValue())) + } + + private fun generateSSOCredentialsJSON(): String { + return """ + { + "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", + "expires_in": 300, + "refresh_token": "refresh-token-value" + } + """.trimIndent() + } + + private fun generateSSOCredentialsJSONWithoutRefreshToken(): String { + return """ + { + "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", + "expires_in": 300 + } + """.trimIndent() + } +} diff --git a/auth0/src/test/java/com/auth0/android/result/SSOCredentialsMock.kt b/auth0/src/test/java/com/auth0/android/result/SSOCredentialsMock.kt index 203fc642..918a9182 100644 --- a/auth0/src/test/java/com/auth0/android/result/SSOCredentialsMock.kt +++ b/auth0/src/test/java/com/auth0/android/result/SSOCredentialsMock.kt @@ -1,19 +1,21 @@ package com.auth0.android.result +import java.util.Date + public class SSOCredentialsMock { public companion object { public fun create( accessToken: String, - idToken:String , + idToken: String, issuedTokenType: String, type: String, refreshToken: String?, - expiresIn: Int + expiresAt: Date ): SSOCredentials { return SSOCredentials( - accessToken,idToken, issuedTokenType, type, expiresIn, refreshToken + accessToken, idToken, issuedTokenType, type, expiresAt, refreshToken ) } } 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 e08361e5..8c770ae5 100755 --- a/auth0/src/test/java/com/auth0/android/util/AuthenticationAPIMockServer.kt +++ b/auth0/src/test/java/com/auth0/android/util/AuthenticationAPIMockServer.kt @@ -96,6 +96,19 @@ internal class AuthenticationAPIMockServer : APIMockServer() { return this } + fun willReturnSuccessfulSSOExchange(): AuthenticationAPIMockServer { + val json = """{ + "access_token": "$ACCESS_TOKEN", + "id_token": "$ID_TOKEN", + "issued_token_type": "urn:auth0:params:oauth:token-type:session-transfer-token", + "token_type": "N_A", + "expires_in": 86000, + "refresh_token": "$REFRESH_TOKEN" + }""" + server.enqueue(responseWithJSON(json, 200)) + return this + } + fun willReturnSuccessfulLoginWithRecoveryCode(): AuthenticationAPIMockServer { val json = """{ "refresh_token": "$REFRESH_TOKEN",