Skip to content

Commit f424b01

Browse files
authored
Add SSOCredentialsDeserializer for proper JSON deserialization of SSOCredentials (#931)
1 parent 91e7b29 commit f424b01

File tree

11 files changed

+253
-24
lines changed

11 files changed

+253
-24
lines changed

V4_MIGRATION_GUIDE.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ v4 of the Auth0 Android SDK includes significant build toolchain updates, update
1515
- [**Breaking Changes**](#breaking-changes)
1616
+ [Classes Removed](#classes-removed)
1717
+ [DPoP Configuration Moved to Builder](#dpop-configuration-moved-to-builder)
18+
+ [SSOCredentials.expiresIn Renamed to expiresAt](#ssocredentialsexpiresin-renamed-to-expiresat)
1819
- [**Default Values Changed**](#default-values-changed)
1920
+ [Credentials Manager minTTL](#credentials-manager-minttl)
2021
- [**Behavior Changes**](#behavior-changes)
@@ -123,6 +124,28 @@ WebAuthProvider
123124
This change ensures that DPoP configuration is scoped to individual login requests rather than
124125
persisting across the entire application lifecycle.
125126

127+
### `SSOCredentials.expiresIn` Renamed to `expiresAt`
128+
129+
**Change:** The `expiresIn` property in `SSOCredentials` has been renamed to `expiresAt` and its type changed from `Int` to `Date`.
130+
131+
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.
132+
133+
**v3:**
134+
135+
```kotlin
136+
val ssoCredentials: SSOCredentials = // ...
137+
val secondsUntilExpiry: Int = ssoCredentials.expiresIn
138+
```
139+
140+
**v4:**
141+
142+
```kotlin
143+
val ssoCredentials: SSOCredentials = // ...
144+
val expirationDate: Date = ssoCredentials.expiresAt
145+
```
146+
147+
**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.
148+
126149
## Default Values Changed
127150

128151
### Credentials Manager `minTTL`

auth0/src/main/java/com/auth0/android/request/internal/GsonProvider.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.auth0.android.request.internal
22

33
import androidx.annotation.VisibleForTesting
44
import com.auth0.android.result.Credentials
5+
import com.auth0.android.result.SSOCredentials
56
import com.auth0.android.result.UserProfile
67
import com.google.gson.Gson
78
import com.google.gson.GsonBuilder
@@ -25,6 +26,7 @@ internal object GsonProvider {
2526
.registerTypeAdapterFactory(JsonRequiredTypeAdapterFactory())
2627
.registerTypeAdapter(UserProfile::class.java, UserProfileDeserializer())
2728
.registerTypeAdapter(Credentials::class.java, CredentialsDeserializer())
29+
.registerTypeAdapter(SSOCredentials::class.java, SSOCredentialsDeserializer())
2830
.registerTypeAdapter(jwksType, JwksDeserializer())
2931
.setDateFormat(DATE_FORMAT)
3032
.create()
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.auth0.android.request.internal
2+
3+
import androidx.annotation.VisibleForTesting
4+
import com.auth0.android.result.SSOCredentials
5+
import com.google.gson.JsonDeserializationContext
6+
import com.google.gson.JsonDeserializer
7+
import com.google.gson.JsonElement
8+
import com.google.gson.JsonParseException
9+
import java.lang.reflect.Type
10+
import java.util.Date
11+
12+
internal open class SSOCredentialsDeserializer : JsonDeserializer<SSOCredentials> {
13+
@Throws(JsonParseException::class)
14+
override fun deserialize(
15+
json: JsonElement,
16+
typeOfT: Type,
17+
context: JsonDeserializationContext
18+
): SSOCredentials {
19+
if (!json.isJsonObject || json.isJsonNull || json.asJsonObject.entrySet().isEmpty()) {
20+
throw JsonParseException("sso credentials json is not a valid json object")
21+
}
22+
val jsonObject = json.asJsonObject
23+
val sessionTransferToken =
24+
context.deserialize<String>(jsonObject.remove("access_token"), String::class.java)
25+
val idToken =
26+
context.deserialize<String>(jsonObject.remove("id_token"), String::class.java)
27+
val issuedTokenType =
28+
context.deserialize<String>(jsonObject.remove("issued_token_type"), String::class.java)
29+
val tokenType =
30+
context.deserialize<String>(jsonObject.remove("token_type"), String::class.java)
31+
val expiresIn =
32+
context.deserialize<Long>(jsonObject.remove("expires_in"), Long::class.java)
33+
val refreshToken =
34+
context.deserialize<String>(jsonObject.remove("refresh_token"), String::class.java)
35+
36+
var expiresInDate: Date?
37+
if (expiresIn != null) {
38+
expiresInDate = Date(currentTimeInMillis + expiresIn * 1000)
39+
} else {
40+
throw JsonParseException("Missing the required property expires_in")
41+
}
42+
43+
return createSSOCredentials(
44+
sessionTransferToken,
45+
idToken,
46+
issuedTokenType,
47+
tokenType,
48+
expiresInDate!!,
49+
refreshToken
50+
)
51+
}
52+
53+
@get:VisibleForTesting
54+
open val currentTimeInMillis: Long
55+
get() = System.currentTimeMillis()
56+
57+
@VisibleForTesting
58+
open fun createSSOCredentials(
59+
sessionTransferToken: String,
60+
idToken: String,
61+
issuedTokenType: String,
62+
tokenType: String,
63+
expiresAt: Date,
64+
refreshToken: String?
65+
): SSOCredentials {
66+
return SSOCredentials(
67+
sessionTransferToken, idToken, issuedTokenType, tokenType, expiresAt, refreshToken
68+
)
69+
}
70+
}

auth0/src/main/java/com/auth0/android/result/SSOCredentials.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.auth0.android.result
22

33
import com.google.gson.annotations.SerializedName
4+
import java.util.Date
45

56
/**
67
* Holds the token credentials required for web SSO.
@@ -46,12 +47,12 @@ public data class SSOCredentials(
4647
@field:SerializedName("token_type") public val tokenType: String,
4748

4849
/**
49-
* Expiration duration of the session transfer token in seconds. Session transfer tokens are short-lived and expire after a few minutes.
50+
* Expiration date of the session transfer token. Session transfer tokens are short-lived and expire after a few minutes.
5051
* Once expired, the session transfer tokens can no longer be used for web SSO.
5152
*
52-
* @return the expiration duration of this session transfer token
53+
* @return the expiration Date of this session transfer token
5354
*/
54-
@field:SerializedName("expires_in") public val expiresIn: Int,
55+
@field:SerializedName("expires_in") public val expiresAt: Date,
5556

5657
/**
5758
* Rotated refresh token. Only available when Refresh Token Rotation is enabled.
@@ -67,6 +68,6 @@ public data class SSOCredentials(
6768
) {
6869

6970
override fun toString(): String {
70-
return "SSOCredentials(sessionTransferToken = ****, idToken = ****,issuedTokenType = $issuedTokenType, tokenType = $tokenType, expiresIn = $expiresIn, refreshToken = ****)"
71+
return "SSOCredentials(sessionTransferToken = ****, idToken = ****,issuedTokenType = $issuedTokenType, tokenType = $tokenType, expiresAt = $expiresAt, refreshToken = ****)"
7172
}
7273
}

auth0/src/test/java/com/auth0/android/authentication/AuthenticationAPIClientTest.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2381,7 +2381,7 @@ public class AuthenticationAPIClientTest {
23812381

23822382
@Test
23832383
public fun shouldSsoExchange() {
2384-
mockAPI.willReturnSuccessfulLogin()
2384+
mockAPI.willReturnSuccessfulSSOExchange()
23852385
val callback = MockAuthenticationCallback<SSOCredentials>()
23862386
client.ssoExchange("refresh-token")
23872387
.start(callback)
@@ -2413,7 +2413,7 @@ public class AuthenticationAPIClientTest {
24132413

24142414
@Test
24152415
public fun shouldSsoExchangeSync() {
2416-
mockAPI.willReturnSuccessfulLogin()
2416+
mockAPI.willReturnSuccessfulSSOExchange()
24172417
val sessionTransferCredentials = client.ssoExchange("refresh-token")
24182418
.execute()
24192419
val request = mockAPI.takeRequest()
@@ -2437,7 +2437,7 @@ public class AuthenticationAPIClientTest {
24372437
@Test
24382438
@ExperimentalCoroutinesApi
24392439
public fun shouldAwaitSsoExchange(): Unit = runTest {
2440-
mockAPI.willReturnSuccessfulLogin()
2440+
mockAPI.willReturnSuccessfulSSOExchange()
24412441
val ssoCredentials = client
24422442
.ssoExchange("refresh-token")
24432443
.await()
@@ -3096,7 +3096,7 @@ public class AuthenticationAPIClientTest {
30963096
whenever(mockKeyStore.hasKeyPair()).thenReturn(true)
30973097
whenever(mockKeyStore.getKeyPair()).thenReturn(Pair(FakeECPrivateKey(), FakeECPublicKey()))
30983098

3099-
mockAPI.willReturnSuccessfulLogin()
3099+
mockAPI.willReturnSuccessfulSSOExchange()
31003100
val callback = MockAuthenticationCallback<SSOCredentials>()
31013101

31023102
client.useDPoP(mockContext).ssoExchange("refresh-token")

auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ public class CredentialsManagerTest {
260260
verifyNoMoreInteractions(storage)
261261
val ssoCredentials = SSOCredentialsMock.create(
262262
"accessToken", "identityToken",
263-
"issuedTokenType", "tokenType", null, 60
263+
"issuedTokenType", "tokenType", null, Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000)
264264
)
265265
manager.saveSsoCredentials(ssoCredentials)
266266
}
@@ -270,7 +270,7 @@ public class CredentialsManagerTest {
270270
verifyNoMoreInteractions(storage)
271271
val ssoCredentials = SSOCredentialsMock.create(
272272
"accessToken", "identityToken",
273-
"issuedTokenType", "tokenType", "refresh_token", 60
273+
"issuedTokenType", "tokenType", "refresh_token", Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000)
274274
)
275275
Mockito.`when`(storage.retrieveString("com.auth0.refresh_token"))
276276
.thenReturn("refresh_token")
@@ -283,7 +283,7 @@ public class CredentialsManagerTest {
283283
verifyNoMoreInteractions(storage)
284284
val ssoCredentials = SSOCredentialsMock.create(
285285
"accessToken", "identityToken",
286-
"issuedTokenType", "tokenType", "refresh_token", 60
286+
"issuedTokenType", "tokenType", "refresh_token", Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000)
287287
)
288288
Mockito.`when`(storage.retrieveString("com.auth0.refresh_token"))
289289
.thenReturn("refresh-token")
@@ -313,14 +313,15 @@ public class CredentialsManagerTest {
313313
.thenReturn("refresh_token_old")
314314
Mockito.`when`(client.ssoExchange("refresh_token_old"))
315315
.thenReturn(SSOCredentialsRequest)
316+
val ssoExpiresAt = Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000)
316317
Mockito.`when`(SSOCredentialsRequest.execute()).thenReturn(
317318
SSOCredentialsMock.create(
318319
"web-sso-token",
319320
"identity-token",
320321
"issued-token-type",
321322
"token-type",
322323
"refresh-token",
323-
60
324+
ssoExpiresAt
324325
)
325326
)
326327
manager.getSsoCredentials(ssoCallback)
@@ -333,7 +334,7 @@ public class CredentialsManagerTest {
333334
MatcherAssert.assertThat(credentials.tokenType, Is.`is`("token-type"))
334335
MatcherAssert.assertThat(credentials.issuedTokenType, Is.`is`("issued-token-type"))
335336
MatcherAssert.assertThat(credentials.refreshToken, Is.`is`("refresh-token"))
336-
MatcherAssert.assertThat(credentials.expiresIn, Is.`is`(60))
337+
MatcherAssert.assertThat(credentials.expiresAt, Is.`is`(ssoExpiresAt))
337338
verify(storage).store("com.auth0.refresh_token", credentials.refreshToken)
338339
}
339340

@@ -409,14 +410,15 @@ public class CredentialsManagerTest {
409410
.thenReturn("refresh_token_old")
410411
Mockito.`when`(client.ssoExchange("refresh_token_old"))
411412
.thenReturn(SSOCredentialsRequest)
413+
val ssoExpiresAt = Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000)
412414
Mockito.`when`(SSOCredentialsRequest.execute()).thenReturn(
413415
SSOCredentialsMock.create(
414416
"web-sso-token",
415417
"identity-token",
416418
"issued-token-type",
417419
"token-type",
418420
"refresh-token",
419-
60
421+
ssoExpiresAt
420422
)
421423
)
422424
val credentials = manager.awaitSsoCredentials()
@@ -425,7 +427,7 @@ public class CredentialsManagerTest {
425427
MatcherAssert.assertThat(credentials.tokenType, Is.`is`("token-type"))
426428
MatcherAssert.assertThat(credentials.issuedTokenType, Is.`is`("issued-token-type"))
427429
MatcherAssert.assertThat(credentials.refreshToken, Is.`is`("refresh-token"))
428-
MatcherAssert.assertThat(credentials.expiresIn, Is.`is`(60))
430+
MatcherAssert.assertThat(credentials.expiresAt, Is.`is`(ssoExpiresAt))
429431
verify(storage).store("com.auth0.refresh_token", credentials.refreshToken)
430432
}
431433

auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ public class SecureCredentialsManagerTest {
187187
verifyNoMoreInteractions(storage)
188188
val ssoCredentials = SSOCredentialsMock.create(
189189
"accessToken", "identityToken",
190-
"issuedTokenType", "tokenType", "refresh_token", 60
190+
"issuedTokenType", "tokenType", "refresh_token", Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000)
191191
)
192192
val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS)
193193
val storedJson = insertTestCredentials(
@@ -209,7 +209,7 @@ public class SecureCredentialsManagerTest {
209209
verifyNoMoreInteractions(storage)
210210
val sessionTransferCredentials = SSOCredentialsMock.create(
211211
"accessToken", "identityToken",
212-
"issuedTokenType", "tokenType", "refresh_token", 60
212+
"issuedTokenType", "tokenType", "refresh_token", Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000)
213213
)
214214
val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS)
215215
insertTestCredentials(
@@ -310,14 +310,15 @@ public class SecureCredentialsManagerTest {
310310
val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS)
311311
Mockito.`when`(client.ssoExchange("refreshToken"))
312312
.thenReturn(SSOCredentialsRequest)
313+
val ssoExpiresAt = Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000)
313314
Mockito.`when`(SSOCredentialsRequest.execute()).thenReturn(
314315
SSOCredentialsMock.create(
315316
"web-sso-token",
316317
"identity-token",
317318
"issued-token-type",
318319
"token-type",
319320
"refresh-token",
320-
60
321+
ssoExpiresAt
321322
)
322323
)
323324
insertTestCredentials(
@@ -347,7 +348,7 @@ public class SecureCredentialsManagerTest {
347348
MatcherAssert.assertThat(credentials.tokenType, Is.`is`("token-type"))
348349
MatcherAssert.assertThat(credentials.issuedTokenType, Is.`is`("issued-token-type"))
349350
MatcherAssert.assertThat(credentials.refreshToken, Is.`is`("refresh-token"))
350-
MatcherAssert.assertThat(credentials.expiresIn, Is.`is`(60))
351+
MatcherAssert.assertThat(credentials.expiresAt, Is.`is`(ssoExpiresAt))
351352
verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture())
352353
val encodedJson = stringCaptor.firstValue
353354
MatcherAssert.assertThat(encodedJson, Is.`is`(Matchers.notNullValue()))
@@ -491,14 +492,15 @@ public class SecureCredentialsManagerTest {
491492
val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS)
492493
Mockito.`when`(client.ssoExchange("refreshToken"))
493494
.thenReturn(SSOCredentialsRequest)
495+
val ssoExpiresAt = Date(CredentialsMock.CURRENT_TIME_MS + 60 * 1000)
494496
Mockito.`when`(SSOCredentialsRequest.execute()).thenReturn(
495497
SSOCredentialsMock.create(
496498
"web-sso-token",
497499
"identity-token",
498500
"issued-token-type",
499501
"token-type",
500502
"refresh-token",
501-
60
503+
ssoExpiresAt
502504
)
503505
)
504506
insertTestCredentials(
@@ -526,7 +528,7 @@ public class SecureCredentialsManagerTest {
526528
MatcherAssert.assertThat(credentials.tokenType, Is.`is`("token-type"))
527529
MatcherAssert.assertThat(credentials.issuedTokenType, Is.`is`("issued-token-type"))
528530
MatcherAssert.assertThat(credentials.refreshToken, Is.`is`("refresh-token"))
529-
MatcherAssert.assertThat(credentials.expiresIn, Is.`is`(60))
531+
MatcherAssert.assertThat(credentials.expiresAt, Is.`is`(ssoExpiresAt))
530532
verify(storage).store(eq("com.auth0.credentials"), stringCaptor.capture())
531533
val encodedJson = stringCaptor.firstValue
532534
MatcherAssert.assertThat(encodedJson, Is.`is`(Matchers.notNullValue()))
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.auth0.android.request.internal
2+
3+
import com.auth0.android.result.CredentialsMock
4+
import com.auth0.android.result.SSOCredentials
5+
import com.auth0.android.result.SSOCredentialsMock
6+
import java.util.*
7+
8+
internal class SSOCredentialsDeserializerMock : SSOCredentialsDeserializer() {
9+
override fun createSSOCredentials(
10+
sessionTransferToken: String,
11+
idToken: String,
12+
issuedTokenType: String,
13+
tokenType: String,
14+
expiresAt: Date,
15+
refreshToken: String?
16+
): SSOCredentials {
17+
return SSOCredentialsMock.create(
18+
sessionTransferToken, idToken, issuedTokenType, tokenType, refreshToken, expiresAt
19+
)
20+
}
21+
22+
override val currentTimeInMillis: Long
23+
get() = CredentialsMock.CURRENT_TIME_MS
24+
}

0 commit comments

Comments
 (0)