BIT-765: Parse user information from JWT token (#127)

This commit is contained in:
Brian Yencho 2023-10-18 09:21:45 -05:00 committed by Álison Fernandes
parent 4f9f0ce8a7
commit 0c50babd23
5 changed files with 165 additions and 0 deletions

View file

@ -0,0 +1,34 @@
package com.x8bit.bitwarden.data.auth.repository.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Contains data that can be parsed from a valid JWT token.
*
* @property userId The ID of the user.
* @property email The user's email address.
* @property isEmailVerified Whether or not the user's email is verified.
* @property name The user's name.
* @property expirationAsEpochTime The expiration time measured as an epoch time in seconds.
* @property hasPremium True if the user has a premium account.
* @property authenticationMethodsReference A list of the authentication methods used during
* authentication.
*/
@Serializable
data class JwtTokenDataJson(
@SerialName("sub")
val userId: String,
@SerialName("email")
val email: String,
@SerialName("email_verified")
val isEmailVerified: Boolean,
@SerialName("name")
val name: String?,
@SerialName("exp")
val expirationAsEpochTime: Int,
@SerialName("premium")
val hasPremium: Boolean,
@SerialName("amr")
val authenticationMethodsReference: List<String>,
)

View file

@ -0,0 +1,28 @@
package com.x8bit.bitwarden.data.auth.repository.util
import com.x8bit.bitwarden.data.auth.repository.model.JwtTokenDataJson
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlDecodeOrNull
import kotlinx.serialization.json.Json
/**
* Internal, generally basic [Json] instance for JWT parsing purposes.
*/
private val json by lazy { Json { ignoreUnknownKeys = true } }
/**
* Parses a [JwtTokenDataJson] from the given [jwtToken], or `null` if this parsing is not possible.
*/
@Suppress("MagicNumber", "ReturnCount")
fun parseJwtTokenDataOrNull(jwtToken: String): JwtTokenDataJson? {
val parts = jwtToken.split(".")
if (parts.size != 3) return null
val dataJson = parts[1]
val decodedDataJson = dataJson.base64UrlDecodeOrNull() ?: return null
return try {
json.decodeFromString<JwtTokenDataJson>(decodedDataJson)
} catch (_: Throwable) {
null
}
}

View file

@ -1,5 +1,7 @@
package com.x8bit.bitwarden.data.platform.datasource.network.util
import okio.ByteString.Companion.decodeBase64
import java.nio.charset.Charset
import java.util.Base64
/**
@ -15,3 +17,18 @@ fun String.base64UrlEncode(): String =
.replace("+", "-")
.replace("/", "_")
.replace("=", "")
/**
* Base 64 decode the given string after making the following replacements:
*
* - replace all "-" with "+"
* - replace all "_" with "/"
*
* A value of `null` will be returned if the decoding fails.
*/
fun String.base64UrlDecodeOrNull(): String? =
this
.replace("-", "+")
.replace("_", "/")
.decodeBase64()
?.string(Charset.defaultCharset())

View file

@ -0,0 +1,43 @@
package com.x8bit.bitwarden.data.auth.repository.util
import com.x8bit.bitwarden.data.auth.repository.model.JwtTokenDataJson
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
class JwtTokenUtilsTest {
@Test
fun `parseJwtTokenDataOrNull for a valid token input should return a JwtTokenData`() {
val testJwtToken =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE2OTc0OTIxMTQsImV4cCI6MTY5NzQ5NTcxN" +
"CwiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eS5iaXR3YXJkZW4uY29tIiwiY2xpZW50X2lkIjoibW9iaWxl" +
"Iiwic3ViIjoiMmExMzViMjMtZTFmYi00MmM5LWJlYzMtNTczODU3YmM4MTgxIiwiYXV0aF90aW1lIjo" +
"xNjk3NDkyMTE0LCJpZHAiOiJiaXR3YXJkZW4iLCJwcmVtaXVtIjpmYWxzZSwiZW1haWwiOiJ0ZXN0QG" +
"JpdHdhcmRlbi5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwic3N0YW1wIjoiSkRIUzRSTUxFNEtGV" +
"EI0TFRIMjVTNkVLRktGTlhOQ0IiLCJuYW1lIjoiQml0d2FyZGVuIFRlc3RlciIsImRldmljZSI6IjNk" +
"ODYxNTU3LWI0Y2MtNDQxZi05YjE4LWM0NTAyYTcxN2UwYiIsImp0aSI6IjA1M0U5NUEzNjFBNEI4QUY" +
"yREEyRDIyNzNDREUxRDVFIiwiaWF0IjoxNjk3NDkyMTE0LCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2" +
"FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.RP2-wABK63Osu-tJY6KJjqVRSJ3-JR_OOdc3N" +
"nm4C5U"
assertEquals(
JwtTokenDataJson(
userId = "2a135b23-e1fb-42c9-bec3-573857bc8181",
email = "test@bitwarden.com",
isEmailVerified = true,
name = "Bitwarden Tester",
expirationAsEpochTime = 1697495714,
hasPremium = false,
authenticationMethodsReference = listOf("Application"),
),
parseJwtTokenDataOrNull(jwtToken = testJwtToken),
)
}
@Test
fun `parseJwtTokenDataOrNull for an invalid token input should return null`() {
assertNull(
parseJwtTokenDataOrNull(jwtToken = "invalid JWT token"),
)
}
}

View file

@ -0,0 +1,43 @@
package com.x8bit.bitwarden.data.platform.datasource.network.util
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
class NetworkUtilsTest {
@Test
fun `base64UrlEncode should Base64 encode the string and make the relevant replacements`() {
// Checks replacement of + to - and removal of =
assertEquals(
"dis-ZA",
"v+>d".base64UrlEncode(),
)
// Checks replacement of \ to _
assertEquals(
"NmI_ImE4",
"6b?\"a8".base64UrlEncode(),
)
}
@Suppress("MaxLineLength")
@Test
fun `base64UrlDecodeOrNull should Base64 decode the string and make the relevant replacements`() {
// Checks replacement of - to +
assertEquals(
"v+>d",
"dis-ZA==".base64UrlDecodeOrNull(),
)
// Checks replacement of _ to \
assertEquals(
"6b?\"a8",
"NmI_ImE4".base64UrlDecodeOrNull(),
)
}
@Test
fun `base64UrlDecodeOrNull should return null value a non-encoded String`() {
assertNull(
"*.*".base64UrlDecodeOrNull(),
)
}
}