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
23 changes: 23 additions & 0 deletions V4_MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SSOCredentials> {
@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<String>(jsonObject.remove("access_token"), String::class.java)
val idToken =
context.deserialize<String>(jsonObject.remove("id_token"), String::class.java)
val issuedTokenType =
context.deserialize<String>(jsonObject.remove("issued_token_type"), String::class.java)
val tokenType =
context.deserialize<String>(jsonObject.remove("token_type"), String::class.java)
val expiresIn =
context.deserialize<Long>(jsonObject.remove("expires_in"), Long::class.java)
val refreshToken =
context.deserialize<String>(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
)
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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 = ****)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2381,7 +2381,7 @@ public class AuthenticationAPIClientTest {

@Test
public fun shouldSsoExchange() {
mockAPI.willReturnSuccessfulLogin()
mockAPI.willReturnSuccessfulSSOExchange()
val callback = MockAuthenticationCallback<SSOCredentials>()
client.ssoExchange("refresh-token")
.start(callback)
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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<SSOCredentials>()

client.useDPoP(mockContext).ssoExchange("refresh-token")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -313,14 +313,15 @@ 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",
"identity-token",
"issued-token-type",
"token-type",
"refresh-token",
60
ssoExpiresAt
)
)
manager.getSsoCredentials(ssoCallback)
Expand All @@ -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)
}

Expand Down Expand Up @@ -409,14 +410,15 @@ 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",
"identity-token",
"issued-token-type",
"token-type",
"refresh-token",
60
ssoExpiresAt
)
)
val credentials = manager.awaitSsoCredentials()
Expand All @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -310,14 +310,15 @@ 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",
"identity-token",
"issued-token-type",
"token-type",
"refresh-token",
60
ssoExpiresAt
)
)
insertTestCredentials(
Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -491,14 +492,15 @@ 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",
"identity-token",
"issued-token-type",
"token-type",
"refresh-token",
60
ssoExpiresAt
)
)
insertTestCredentials(
Expand Down Expand Up @@ -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()))
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading