Skip to content

Commit

Permalink
Create LegacyCredentialsMigrator to help loading old credentials vers…
Browse files Browse the repository at this point in the history
…ions

We've changed the Credentials type twice, so there are two legacy formats of that type that we possibly encounter when loading data from EncryptedSharedPreferences.
This class and the legacy types takes care of trying and deserializing them in a clean way, so we don't have to pollute our DefaultTokenStore with the necessary logic.

This also means reintroducing the kotlinx.datetime dependency - however, this won't lead to any issues with 3rd party apps, and our own one is protected since it's using desugaring.
  • Loading branch information
michpohl committed Aug 22, 2024
1 parent b600d6c commit 621e84f
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 75 deletions.
1 change: 1 addition & 0 deletions auth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies {
implementation(libs.kotlinxCoroutinesAndroid)
implementation(libs.kotlinxCoroutinesCore)
implementation(libs.kotlinx.coroutines.test)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.serialization.retrofit.converter)
implementation(libs.okhttp.loggingInterceptor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ package com.tidal.sdk.auth.storage
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import com.tidal.sdk.auth.model.Tokens
import com.tidal.sdk.common.logger
import com.tidal.sdk.common.w
import com.tidal.sdk.auth.storage.legacycredentials.LegacyCredentialsMigrator
import javax.inject.Inject
import kotlinx.serialization.decodeFromString as decode
import kotlinx.serialization.encodeToString as encode
Expand Down Expand Up @@ -35,27 +34,19 @@ internal class DefaultTokensStore @Inject constructor(
}

@Suppress("TooGenericExceptionCaught", "SwallowedException")
private fun loadTokens(): Tokens? {
return encryptedSharedPreferences.getString(credentialsKey, null)?.let {
try {
Json.decode<Tokens>(it)
} catch (e: Exception) {
logger.w { " Failed to decode tokens. Attempting to decode legacy tokens" }
decodeLegacyTokens(it).also { convertedLegacyTokens ->
saveTokens(convertedLegacyTokens)
}
}
private fun loadTokens(): Tokens? = encryptedSharedPreferences.getString(
credentialsKey,
null,
)?.let {
try {
Json.decode<Tokens>(it)
} catch (e: Exception) {
LegacyCredentialsMigrator().migrateCredentials(it)
}.also { tokens ->
saveTokens(tokens)
}
}

/**
* This method is used to decode tokens that were stored in the old format, using [Scopes].
* This is used to ensure backwards compatibility.
*/
private fun decodeLegacyTokens(jsonString: String): Tokens {
return Json.decode<LegacyTokens>(jsonString).toTokens()
}

override fun saveTokens(tokens: Tokens) {
val stringToSave = Json.encode(tokens)
encryptedSharedPreferences.edit().putString(credentialsKey, stringToSave).apply().also {
Expand Down

This file was deleted.

17 changes: 0 additions & 17 deletions auth/src/main/kotlin/com/tidal/sdk/auth/storage/LegacyTokens.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.tidal.sdk.auth.storage.legacycredentials

import com.tidal.sdk.auth.model.Credentials
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable

@Serializable
sealed class LegacyCredentials {
abstract fun toCredentials(): Credentials
}

/**
* Represents the credentials of a user or client.
*/
@Deprecated("Use [Credentials] instead.")
@Serializable
data class LegacyCredentialsV1(
val clientId: String,
val requestedScopes: Scopes,
val clientUniqueKey: String?,
val grantedScopes: Scopes,
val userId: String?,
val expires: Instant?,
val token: String?,
) : LegacyCredentials() {
override fun toCredentials(): Credentials = Credentials(
clientId,
requestedScopes.scopes,
clientUniqueKey,
grantedScopes.scopes,
userId,
expires?.epochSeconds,
token,
)
}

/**
* Represents the credentials of a user or client.
*/
@Deprecated("Use [Credentials] instead.")
@Serializable
data class LegacyCredentialsV2(
val clientId: String,
val requestedScopes: Set<String>,
val clientUniqueKey: String?,
val grantedScopes: Set<String>,
val userId: String?,
val expires: Instant?,
val token: String?,
) : LegacyCredentials() {
override fun toCredentials(): Credentials = Credentials(
clientId,
requestedScopes,
clientUniqueKey,
grantedScopes,
userId,
expires?.epochSeconds,
token,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.tidal.sdk.auth.storage.legacycredentials

import com.tidal.sdk.auth.model.Tokens
import com.tidal.sdk.common.e
import com.tidal.sdk.common.i
import com.tidal.sdk.common.logger
import kotlinx.serialization.json.Json

internal class LegacyCredentialsMigrator {

private inline fun <reified T : LegacyTokens> decodeJsonString(jsonString: String): Tokens =
Json.decodeFromString<T>(
jsonString,
).toTokens()

@Suppress("TooGenericExceptionCaught")
fun migrateCredentials(jsonString: String): Tokens {
// if ever necessary, add further legacy type operations here
val operations = listOf(
{ decodeJsonString<TokensV1>(jsonString) },
{ decodeJsonString<TokensV2>(jsonString) },
)
logger.i { "Attempting to decode using legacy types." }
val exceptions = mutableListOf<Exception>()
for (operation in operations) {
try {
return operation().also {
println("Successfully decoded using legacy types.")
}
} catch (e: Exception) {
logger.i { "Failed to decode using legacy types." }
println("Failed to decode using legacy types.")
exceptions.plus(e)
}
}
logger.e { "Failed to decode using legacy types! Exceptions caught:" }
exceptions.forEach { logger.e { it } }
throw exceptions.first()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.tidal.sdk.auth.storage.legacycredentials

import com.tidal.sdk.auth.model.Tokens
import kotlinx.serialization.Serializable

internal sealed class LegacyTokens {
abstract fun toTokens(): Tokens
}

@Serializable
internal data class TokensV1(
val credentials: LegacyCredentialsV1,
val refreshToken: String? = null,
) : LegacyTokens() {
override fun toTokens(): Tokens = Tokens(
credentials.toCredentials(),
refreshToken,
)
}

@Serializable
internal data class TokensV2(
val credentials: LegacyCredentialsV2,
val refreshToken: String? = null,
) : LegacyTokens() {
override fun toTokens(): Tokens = Tokens(
credentials.toCredentials(),
refreshToken,
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.tidal.sdk.auth.storage
package com.tidal.sdk.auth.storage.legacycredentials

import kotlinx.serialization.Serializable

Expand All @@ -14,9 +14,7 @@ data class Scopes(val scopes: Set<String>) {
* Returns a string representation of the scopes that is readable by the TIDAL API backend.
* @return The string representation of the scopes.
*/
override fun toString(): String {
return scopes.joinToString(" ")
}
override fun toString(): String = scopes.joinToString(" ")

companion object {

Expand All @@ -25,8 +23,6 @@ data class Scopes(val scopes: Set<String>) {
* @param joinedString The string representation of the scopes.
* @return The Scopes object.
*/
fun fromString(joinedString: String): Scopes {
return Scopes(joinedString.split(" ").toSet())
}
fun fromString(joinedString: String): Scopes = Scopes(joinedString.split(" ").toSet())
}
}

0 comments on commit 621e84f

Please sign in to comment.