From a6a6194ea05ee1fd249233c91d2cd332f3b293f9 Mon Sep 17 00:00:00 2001 From: Jaewoong Eum Date: Wed, 25 Oct 2023 14:56:07 +0900 Subject: [PATCH] Support generating development token for a development environment (#888) * new codegen * Imlpement token utils and unit tests * Implement devToken --------- Co-authored-by: Tommaso Barbugli Co-authored-by: Thierry Schellenbach --- README.md | 1 + .../ui/components/avatar/UserAvatar.kt | 3 +- .../api/stream-video-android-core.api | 1 + .../video/android/core/StreamVideo.kt | 8 + .../video/android/core/utils/TokenUtils.kt | 69 ++++++++ .../android/core/utils/TokenUtilsTest.kt | 153 ++++++++++++++++++ 6 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/TokenUtils.kt create mode 100644 stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/utils/TokenUtilsTest.kt diff --git a/README.md b/README.md index d879e38057..f433ff5e9d 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ Video roadmap and changelog is available [here](https://github.com/GetStream/pro ### 0.5.0 milestone +- [X] Development token to support a development environment - [ ] H264 workaround on Samsung 23? (see https://github.com/livekit/client-sdk-android/blob/main/livekit-android-sdk/src/main/java/io/livekit/android/webrtc/SimulcastVideoEncoderFactoryWrapper.kt#L34 and - https://github.com/react-native-webrtc/react-native-webrtc/issues/983#issuecomment-975624906) - [ ] Test coverage diff --git a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatar.kt b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatar.kt index ed3191adda..d91a5c3578 100644 --- a/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatar.kt +++ b/stream-video-android-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/avatar/UserAvatar.kt @@ -44,7 +44,8 @@ import io.getstream.video.android.model.User * * Based on the state within the [User], we either show an image or their initials. * - * @param user The user whose avatar we want to show. + * @param userName The user name whose avatar we want to show. + * @param userImage The user image whose avatar we want to show. * @param modifier Modifier for styling. * @param shape The shape of the avatar. * @param textStyle The [TextStyle] that will be used for the initials. diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index 5293159365..0794454784 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -729,6 +729,7 @@ public abstract interface class io/getstream/video/android/core/StreamVideo : io public abstract fun connectAsync (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun createDevice (Lio/getstream/android/push/PushDevice;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun deleteDevice (Lio/getstream/video/android/model/Device;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun devToken (Ljava/lang/String;)Ljava/lang/String; public abstract fun getContext ()Landroid/content/Context; public abstract fun getEdges (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun getState ()Lio/getstream/video/android/core/ClientState; diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideo.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideo.kt index 780df16a5b..6ba52e539d 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideo.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideo.kt @@ -27,6 +27,7 @@ import io.getstream.video.android.core.model.QueriedCalls import io.getstream.video.android.core.model.QueriedMembers import io.getstream.video.android.core.model.SortField import io.getstream.video.android.core.notifications.NotificationHandler +import io.getstream.video.android.core.utils.TokenUtils import io.getstream.video.android.model.Device import io.getstream.video.android.model.User import kotlinx.coroutines.Deferred @@ -216,6 +217,13 @@ public interface StreamVideo : NotificationHandler { } } + /** + * Generate a developer token that can be used to connect users while the app is using a development environment. + * + * @param userId the desired id of the user to be connected. + */ + public fun devToken(userId: String): String = TokenUtils.devToken(userId) + public fun cleanup() } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/TokenUtils.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/TokenUtils.kt new file mode 100644 index 0000000000..c61d9c2cc2 --- /dev/null +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/utils/TokenUtils.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.core.utils + +import android.util.Base64 +import io.getstream.log.taggedLogger +import org.json.JSONException +import org.json.JSONObject +import java.nio.charset.StandardCharsets + +internal object TokenUtils { + + val logger by taggedLogger("Video:TokenUtils") + + fun getUserId(token: String): String = try { + JSONObject( + token + .takeIf { it.contains(".") } + ?.split(".") + ?.getOrNull(1) + ?.let { + String( + Base64.decode( + it.toByteArray(StandardCharsets.UTF_8), + Base64.NO_WRAP, + ), + ) + } + ?: "", + ).optString("user_id") + } catch (e: JSONException) { + logger.e(e) { "Unable to obtain userId from JWT Token Payload" } + "" + } catch (e: IllegalArgumentException) { + logger.e(e) { "Unable to obtain userId from JWT Token Payload" } + "" + } + + /** + * Generate a developer token that can be used to connect users while the app is using a development environment. + * + * @param userId the desired id of the user to be connected. + */ + fun devToken(userId: String): String { + require(userId.isNotEmpty()) { "User id must not be empty" } + val header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" // {"alg": "HS256", "typ": "JWT"} + val devSignature = "devtoken" + val payload: String = + Base64.encodeToString( + "{\"user_id\":\"$userId\"}".toByteArray(StandardCharsets.UTF_8), + Base64.NO_WRAP, + ) + return "$header.$payload.$devSignature" + } +} diff --git a/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/utils/TokenUtilsTest.kt b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/utils/TokenUtilsTest.kt new file mode 100644 index 0000000000..8816cf21f7 --- /dev/null +++ b/stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/utils/TokenUtilsTest.kt @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2014-2023 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-video-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.video.android.core.utils + +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.ParameterizedRobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.test.assertEquals + +@RunWith(ParameterizedRobolectricTestRunner::class) +@Config(manifest = Config.NONE) +internal class TokenUtilsTest( + private val token: String, + private val expectedUserId: String, +) { + + @Test + fun `Should return userId inside of the token`() { + assertEquals(TokenUtils.getUserId(token), expectedUserId) + } + + companion object { + + @Suppress("MaxLineLength") + @JvmStatic + @ParameterizedRobolectricTestRunner.Parameters(name = "{index}: {0} => {1}") + fun data(): Collection> = listOf( + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiamMifQ==.devtoken", + "jc", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0=.devtoken", + "vishal", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYW1pbiJ9.devtoken", + "amin", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMWYzN2U1OGQtZDhiMC00NzZhLWE0ZjItZjg2MTFlMGQ4NWQ5In0.l3u9P1NKhJ91rI1tzOcABGh045Kj69-iVkC2yUtohVw", + "1f37e58d-d8b0-476a-a4f2-f8611e0d85d9", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiRnJhIn0.ENQGHEsAL3WjVhd_qTiJa_9ojGKi2ftJ8xlocT8SVX4", + "Fra", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNmQ5NTI3M2ItMzNmMC00MGY1LWIwN2MtMGRhMjYxMDkyMDc0In0.lT5O4EmWzhRKPTau6dHP4F6M42EA2aN_8-iAPuiFPLc", + "6d95273b-33f0-40f5-b07c-0da261092074", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMWUzMzAxMTEtNjcwZC00OWE3LThmMDgtZTY3MzQzMzhjNjQxIn0.YEFdEMWj5rurQKr0QMrvO72jGZHU-AlpUIbyY4jxYdU", + "1e330111-670d-49a7-8f08-e6734338c641", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMjllNDZkZWYtODhmNC00YjZhLWExMGMtNTg0ZDEwYzRmZGM5In0.Mxr4Prnb1-EVM5NSSP2EugLApSChoKnVFwe7ZO15V_U", + "29e46def-88f4-4b6a-a10c-584d10c4fdc9", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMWYwNTJjMDgtZjY4Mi00YTgzLTg5NmMtOWYxOWE2OGJkMmJiIn0.L-cQ-DYubOzFpsg94OEwlTRYjat9G4cqfAgzBPALW0g", + "1f052c08-f682-4a83-896c-9f19a68bd2bb", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMGQzZTZlNjMtNjIwMC00ZGQxLWE4NDEtNDA1MDY2NDg5MWUyIn0.osFIgnle17f6yEkK7rPJguQaKhOiawAO3BylYaiRTqE", + "0d3e6e63-6200-4dd1-a841-4050664891e2", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMTJmYjBlZDktOTNkOC00OGE1LTk4ODUtMjhlNDFmMmU0YzQzIn0.t_oc_DEwTav7ni0z4bi8Xla_5Zj5cI6l3rKxwoCvtB0", + "12fb0ed9-93d8-48a5-9885-28e41f2e4c43", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNTUzMWE4Y2ItM2I4MS00YTU0LWI0MjQtN2FlNGUyN2JmOGJhIn0.PXkmukg3JU4igH_YUMr7WC7a1EcwKBr_C5V2ouBlmIs", + "5531a8cb-3b81-4a54-b424-7ae4e27bf8ba", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMDYzNTY1NjQtMTQ5Zi00YjJjLTg1MjUtZDIyMDU2ZmVjNDA0In0.R3-HY9Cno62yIhCjLXDBR8LF7y1udwX8m4LLNP2dIZo", + "06356564-149f-4b2c-8525-d22056fec404", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiYWQ3ZDkzMTQtNTA3MS00ZDYxLTk4YTEtZmZhNjQzY2U4MjRhIn0.iF4UWGFtX0eTAIBTCum7fjD_TKn8wjEqb3PVxJrwbuM", + "ad7d9314-5071-4d61-98a1-ffa643ce824a", + ), + arrayOf( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY2ViZjU2MmEtNDgwNi00YzY0LWE4MjctNTlkNTBhYWM0MmJhIn0.kuXab7RhQRHdsErEW5tTN_mmuyLPNU4ZbprvuPXM4OY", + "cebf562a-4806-4c64-a827-59d50aac42ba", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MCJ9.Vow00KvvhLvWRZIPKomXQOYpBL_P-_-eDeDKmBRvEj4", + "qatest0", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MSJ9.H1nlYibjgp1HfaOd0sA_T4038tjsN61mJWxvUjmRQI0", + "qatest1", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MiJ9.GYp9ikLtU2eG9Mq7tmHThzbV7C8W82j18sExuO7-ogc", + "qatest2", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MyJ9.kLZJz5kl7e3Zw7i2T39Yp05_nAmh9RGG0rt6-5zOpfE", + "qatest3", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.kLZJz5kl7e3Zw7i2T39Yp05_nAmh9RGG0rt6-5zOpfE", + "", + ), + arrayOf( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", + "", + ), + arrayOf( + randomString(), + "", + ), + arrayOf( + "${randomString()}.", + "", + ), + arrayOf( + "${randomString()}.${randomString()}", + "", + ), + arrayOf( + "", + "", + ), + ) + } +} + +private val charPool: CharArray = (('a'..'z') + ('A'..'Z') + ('0'..'9')).toCharArray() + +private fun randomString(size: Int = 20): String = buildString(capacity = size) { + repeat(size) { + append(charPool.random()) + } +}