mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
BIT-765: Parse user information from JWT token (#127)
This commit is contained in:
parent
4f9f0ce8a7
commit
0c50babd23
5 changed files with 165 additions and 0 deletions
|
@ -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>,
|
||||
)
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
|
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue