Skip to content
Snippets Groups Projects
Unverified Commit e0dc965d authored by chris-cwa's avatar chris-cwa Committed by GitHub
Browse files

Persist result from otp authorization server (EXPOSUREAPP-4637) (#2324)

parent 360d1674
No related branches found
No related tags found
No related merge requests found
package de.rki.coronawarnapp.datadonation
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import org.joda.time.Instant
import java.util.UUID
@Keep
data class OTPAuthorizationResult(
@SerializedName("uuid")
val uuid: UUID,
@SerializedName("authorized")
val authorized: Boolean,
@SerializedName("redeemedAt")
val redeemedAt: Instant
)
package de.rki.coronawarnapp.datadonation.storage package de.rki.coronawarnapp.datadonation.storage
import de.rki.coronawarnapp.datadonation.OTPAuthorizationResult
import de.rki.coronawarnapp.datadonation.OneTimePassword import de.rki.coronawarnapp.datadonation.OneTimePassword
import de.rki.coronawarnapp.datadonation.survey.SurveySettings import de.rki.coronawarnapp.datadonation.survey.SurveySettings
import javax.inject.Inject import javax.inject.Inject
...@@ -10,14 +11,25 @@ class OTPRepository @Inject constructor( ...@@ -10,14 +11,25 @@ class OTPRepository @Inject constructor(
private val surveySettings: SurveySettings private val surveySettings: SurveySettings
) { ) {
val lastOTP: OneTimePassword? val otp: OneTimePassword?
get() = surveySettings.oneTimePassword get() = surveySettings.oneTimePassword
var otpAuthorizationResult: OTPAuthorizationResult?
get() = surveySettings.otpAuthorizationResult
set(value) {
surveySettings.otpAuthorizationResult = value
// since we have a result from authorization server we must not use the generated otp again
surveySettings.oneTimePassword = null
}
fun generateOTP(): OneTimePassword = OneTimePassword().also { fun generateOTP(): OneTimePassword = OneTimePassword().also {
surveySettings.oneTimePassword = it surveySettings.oneTimePassword = it
// only one otp can be stored at a time - remove authorization result of older otp
surveySettings.otpAuthorizationResult = null
} }
fun clear() { fun clear() {
surveySettings.oneTimePassword = null surveySettings.oneTimePassword = null
surveySettings.otpAuthorizationResult = null
} }
} }
...@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.datadonation.survey ...@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.datadonation.survey
import android.content.Context import android.content.Context
import com.google.gson.Gson import com.google.gson.Gson
import de.rki.coronawarnapp.datadonation.OTPAuthorizationResult
import de.rki.coronawarnapp.datadonation.OneTimePassword import de.rki.coronawarnapp.datadonation.OneTimePassword
import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.di.AppContext
import de.rki.coronawarnapp.util.preferences.clearAndNotify import de.rki.coronawarnapp.util.preferences.clearAndNotify
...@@ -42,7 +43,31 @@ class SurveySettings @Inject constructor( ...@@ -42,7 +43,31 @@ class SurveySettings @Inject constructor(
.putString(KEY_OTP, if (value == null) null else gson.toJson(value)) .putString(KEY_OTP, if (value == null) null else gson.toJson(value))
.apply() .apply()
var otpAuthorizationResult: OTPAuthorizationResult?
get() {
try {
val json = preferences.getString(KEY_OTP_RESULT, null)
if (json != null) {
val result = gson.fromJson(json, OTPAuthorizationResult::class.java)
requireNotNull(result.uuid)
requireNotNull(result.authorized)
requireNotNull(result.redeemedAt)
return result
}
return null
} catch (t: Throwable) {
Timber.e(t, "failed to parse OTP from preferences")
return null
}
}
set(value) =
preferences
.edit()
.putString(KEY_OTP_RESULT, if (value == null) null else gson.toJson(value))
.apply()
fun clear() = preferences.clearAndNotify() fun clear() = preferences.clearAndNotify()
} }
private const val KEY_OTP = "one_time_password" private const val KEY_OTP = "one_time_password"
private const val KEY_OTP_RESULT = "otp_result"
...@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.datadonation.storage ...@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.datadonation.storage
import android.content.Context import android.content.Context
import com.google.gson.Gson import com.google.gson.Gson
import de.rki.coronawarnapp.datadonation.OTPAuthorizationResult
import de.rki.coronawarnapp.datadonation.OneTimePassword import de.rki.coronawarnapp.datadonation.OneTimePassword
import de.rki.coronawarnapp.datadonation.survey.SurveySettings import de.rki.coronawarnapp.datadonation.survey.SurveySettings
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
...@@ -38,7 +39,7 @@ class OTPRepositoryTest : BaseTest() { ...@@ -38,7 +39,7 @@ class OTPRepositoryTest : BaseTest() {
val uuid = UUID.fromString("e103c755-0975-4588-a639-d0cd1ba421a0") val uuid = UUID.fromString("e103c755-0975-4588-a639-d0cd1ba421a0")
val time = Instant.ofEpochMilli(1612381131014) val time = Instant.ofEpochMilli(1612381131014)
every { surveySettings.oneTimePassword } returns OneTimePassword(uuid, time) every { surveySettings.oneTimePassword } returns OneTimePassword(uuid, time)
val lastOTP = OTPRepository(surveySettings).lastOTP val lastOTP = OTPRepository(surveySettings).otp
lastOTP shouldNotBe null lastOTP shouldNotBe null
lastOTP!!.apply { lastOTP!!.apply {
uuid shouldBe uuid uuid shouldBe uuid
...@@ -58,6 +59,36 @@ class OTPRepositoryTest : BaseTest() { ...@@ -58,6 +59,36 @@ class OTPRepositoryTest : BaseTest() {
@Test @Test
fun `no last otp`() { fun `no last otp`() {
every { surveySettings.oneTimePassword } returns null every { surveySettings.oneTimePassword } returns null
OTPRepository(surveySettings).lastOTP shouldBe null OTPRepository(surveySettings).otp shouldBe null
}
@Test
fun `no otp auth result after generating new otp`() {
every { context.getSharedPreferences("survey_localdata", Context.MODE_PRIVATE) } returns MockSharedPreferences()
val settings = SurveySettings(context, Gson())
settings.otpAuthorizationResult = OTPAuthorizationResult(
UUID.randomUUID(),
true,
Instant.now()
)
settings.otpAuthorizationResult shouldNotBe null
OTPRepository(settings).generateOTP()
settings.otpAuthorizationResult shouldBe null
}
@Test
fun `no otp after storing otp auth result`() {
every { context.getSharedPreferences("survey_localdata", Context.MODE_PRIVATE) } returns MockSharedPreferences()
val settings = SurveySettings(context, Gson())
settings.oneTimePassword = OneTimePassword(UUID.randomUUID(), Instant.now())
settings.oneTimePassword shouldNotBe null
OTPRepository(settings).otpAuthorizationResult = OTPAuthorizationResult(
UUID.randomUUID(),
true,
Instant.now()
)
settings.oneTimePassword shouldBe null
} }
} }
...@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.datadonation.survey ...@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.datadonation.survey
import android.content.Context import android.content.Context
import com.google.gson.Gson import com.google.gson.Gson
import de.rki.coronawarnapp.datadonation.OTPAuthorizationResult
import de.rki.coronawarnapp.datadonation.OneTimePassword import de.rki.coronawarnapp.datadonation.OneTimePassword
import de.rki.coronawarnapp.util.serialization.SerializationModule import de.rki.coronawarnapp.util.serialization.SerializationModule
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
...@@ -39,7 +40,7 @@ class SurveySettingsTest : BaseTest() { ...@@ -39,7 +40,7 @@ class SurveySettingsTest : BaseTest() {
} }
@Test @Test
fun `load and deserialize json`() { fun `load and deserialize otp json`() {
val instance = SurveySettings(context, baseGson) val instance = SurveySettings(context, baseGson)
instance.oneTimePassword shouldBe null instance.oneTimePassword shouldBe null
...@@ -60,7 +61,7 @@ class SurveySettingsTest : BaseTest() { ...@@ -60,7 +61,7 @@ class SurveySettingsTest : BaseTest() {
} }
@Test @Test
fun `parsing error`() { fun `otp parsing error`() {
val instance = SurveySettings(context, baseGson) val instance = SurveySettings(context, baseGson)
instance.oneTimePassword shouldBe null instance.oneTimePassword shouldBe null
...@@ -73,7 +74,7 @@ class SurveySettingsTest : BaseTest() { ...@@ -73,7 +74,7 @@ class SurveySettingsTest : BaseTest() {
} }
@Test @Test
fun `save and serialize json`() { fun `save and serialize otp json`() {
val uuid = UUID.fromString("e103c755-0975-4588-a639-d0cd1ba421a0") val uuid = UUID.fromString("e103c755-0975-4588-a639-d0cd1ba421a0")
val time = Instant.ofEpochMilli(1612381567242) val time = Instant.ofEpochMilli(1612381567242)
...@@ -88,4 +89,59 @@ class SurveySettingsTest : BaseTest() { ...@@ -88,4 +89,59 @@ class SurveySettingsTest : BaseTest() {
} }
""".trimIndent() """.trimIndent()
} }
@Test
fun `load and deserialize auth result json`() {
val instance = SurveySettings(context, baseGson)
instance.otpAuthorizationResult shouldBe null
preferences.edit().putString(
"otp_result",
"""
{
"uuid":"e103c755-0975-4588-a639-d0cd1ba421a1",
"authorized": true,
"redeemedAt": 1612381217443
}
""".trimIndent()
).apply()
val value = instance.otpAuthorizationResult
value shouldNotBe null
value!!.uuid.toString() shouldBe "e103c755-0975-4588-a639-d0cd1ba421a1"
value.authorized shouldBe true
value.redeemedAt.millis shouldBe 1612381217443
}
@Test
fun `auth result parsing error`() {
val instance = SurveySettings(context, baseGson)
instance.otpAuthorizationResult shouldBe null
preferences
.edit()
.putString("otp_result", "invalid value")
.apply()
instance.otpAuthorizationResult shouldBe null
}
@Test
fun `save and serialize auth result json`() {
val uuid = UUID.fromString("e103c755-0975-4588-a639-d0cd1ba421a0")
val authorized = false
val redeemedAt = Instant.ofEpochMilli(1612381217445)
val instance = SurveySettings(context, baseGson)
instance.otpAuthorizationResult = OTPAuthorizationResult(uuid, authorized, redeemedAt)
val value = preferences.getString("otp_result", null)
value shouldBe """
{
"uuid": "e103c755-0975-4588-a639-d0cd1ba421a0",
"authorized": false,
"redeemedAt": 1612381217445
}
""".trimIndent()
}
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment