Skip to content

Commit 28da02c

Browse files
tomtitAlexey Nechaev
authored andcommitted
Fixes #7758: Fixed JWT token for Jitsi openidtoken-jwt authentication
Signed-off-by: Alexey Nechaev <seysane@yahoo.com>
1 parent 40bbd3e commit 28da02c

File tree

4 files changed

+175
-2
lines changed

4 files changed

+175
-2
lines changed

changelog.d/7758.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed JWT token for Jitsi openidtoken-jwt authentication

vector/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ dependencies {
282282
runtimeOnly(libs.jsonwebtoken.jjwtOrgjson) {
283283
exclude group: 'org.json', module: 'json' //provided by Android natively
284284
}
285+
testImplementation(libs.jsonwebtoken.jjwtOrgjson)
285286
implementation 'commons-codec:commons-codec:1.15'
286287

287288
// MapTiler

vector/src/main/java/im/vector/app/features/call/conference/jwt/JitsiJWTFactory.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ package im.vector.app.features.call.conference.jwt
1919
import im.vector.app.core.utils.ensureProtocol
2020
import io.jsonwebtoken.Jwts
2121
import io.jsonwebtoken.SignatureAlgorithm
22+
import io.jsonwebtoken.io.Encoders
2223
import io.jsonwebtoken.security.Keys
2324
import org.matrix.android.sdk.api.session.openid.OpenIdToken
25+
import javax.crypto.Mac
26+
import javax.crypto.spec.SecretKeySpec
2427
import javax.inject.Inject
2528

2629
class JitsiJWTFactory @Inject constructor() {
@@ -37,7 +40,12 @@ class JitsiJWTFactory @Inject constructor() {
3740
userDisplayName: String
3841
): String {
3942
// The secret key here is irrelevant, we're only using the JWT to transport data to Prosody in the Jitsi stack.
40-
val key = Keys.secretKeyFor(SignatureAlgorithm.HS256)
43+
// In the PR https://github.com/jitsi/luajwtjitsi/pull/3 the function `luajwtjitsi.decode` was removed and
44+
// we cannot use random secret keys anymore. But the JWT library `jjwt` doesn't accept the hardcoded key `notused`
45+
// from the module `prosody-mod-auth-matrix-user-verification` since it's too short and thus insecure. So, we
46+
// create a new token using a random key and then re-sign the token manually with the 'weak' key.
47+
val signatureAlgorithm = SignatureAlgorithm.HS256
48+
val key = Keys.secretKeyFor(signatureAlgorithm)
4149
val context = mapOf(
4250
"matrix" to mapOf(
4351
"token" to openIdToken.accessToken,
@@ -52,7 +60,8 @@ class JitsiJWTFactory @Inject constructor() {
5260
// As per Jitsi token auth, `iss` needs to be set to something agreed between
5361
// JWT generating side and Prosody config. Since we have no configuration for
5462
// the widgets, we can't set one anywhere. Using the Jitsi domain here probably makes sense.
55-
return Jwts.builder()
63+
val token = Jwts.builder()
64+
.setHeaderParam("typ", "JWT")
5665
.setIssuer(jitsiServerDomain)
5766
.setSubject(jitsiServerDomain)
5867
.setAudience(jitsiServerDomain.ensureProtocol())
@@ -61,5 +70,11 @@ class JitsiJWTFactory @Inject constructor() {
6170
.claim("context", context)
6271
.signWith(key)
6372
.compact()
73+
// Re-sign token with the hardcoded key
74+
val toSign = token.substring(0, token.lastIndexOf('.'))
75+
val mac = Mac.getInstance(signatureAlgorithm.jcaName)
76+
mac.init(SecretKeySpec("notused".toByteArray(), mac.algorithm))
77+
val prosodySignature = Encoders.BASE64URL.encode(mac.doFinal(toSign.toByteArray()))
78+
return "$toSign.$prosodySignature"
6479
}
6580
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.call.conference.jwt
18+
19+
import com.squareup.moshi.JsonAdapter
20+
import com.squareup.moshi.Moshi
21+
import com.squareup.moshi.Types
22+
import org.junit.Assert.assertEquals
23+
import org.junit.Assert.assertFalse
24+
import org.junit.Assert.assertTrue
25+
import org.junit.Before
26+
import org.junit.Test
27+
import org.matrix.android.sdk.api.session.openid.OpenIdToken
28+
import java.lang.reflect.ParameterizedType
29+
import java.util.Base64
30+
import kotlin.streams.toList
31+
32+
class JitsiJWTFactoryTest {
33+
private val base64Decoder = Base64.getUrlDecoder()
34+
private val moshi = Moshi.Builder().build()
35+
private val stringToString = Types.newParameterizedType(Map::class.java, String::class.java, String::class.java)
36+
private val stringToAny = Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java)
37+
private lateinit var factory: JitsiJWTFactory
38+
39+
@Before
40+
fun init() {
41+
factory = JitsiJWTFactory()
42+
}
43+
44+
@Test
45+
fun `token contains 3 encoded parts`() {
46+
val token = createToken()
47+
48+
val parts = token.split(".")
49+
assertEquals(3, parts.size)
50+
parts.forEach {
51+
assertTrue("Non-empty array", base64Decoder.decode(it).isNotEmpty())
52+
}
53+
}
54+
55+
@Test
56+
fun `token contains unique signature`() {
57+
val signatures = listOf("one", "two").stream()
58+
.map { createToken(it) }
59+
.map { it.split(".")[2] }
60+
.map { base64Decoder.decode(it) }
61+
.toList()
62+
63+
assertEquals(2, signatures.size)
64+
signatures.forEach {
65+
assertEquals(32, it.size)
66+
}
67+
assertFalse("Unique", signatures[0].contentEquals(signatures[1]))
68+
}
69+
70+
@Test
71+
fun `token header contains algorithm`() {
72+
val token = createToken()
73+
74+
assertEquals("HS256", parseTokenHeader(token)["alg"])
75+
}
76+
77+
@Test
78+
fun `token header contains type`() {
79+
val token = createToken()
80+
81+
assertEquals("JWT", parseTokenHeader(token)["typ"])
82+
}
83+
84+
@Test
85+
fun `token body contains subject`() {
86+
val token = createToken()
87+
88+
assertEquals("jitsi-server-domain", parseTokenBody(token)["sub"])
89+
}
90+
91+
@Test
92+
fun `token body contains issuer`() {
93+
val token = createToken()
94+
95+
assertEquals("jitsi-server-domain", parseTokenBody(token)["iss"])
96+
}
97+
98+
@Test
99+
fun `token body contains audience`() {
100+
val token = createToken()
101+
102+
assertEquals("https://jitsi-server-domain", parseTokenBody(token)["aud"])
103+
}
104+
105+
@Test
106+
fun `token body contains room claim`() {
107+
val token = createToken()
108+
109+
assertEquals("*", parseTokenBody(token)["room"])
110+
}
111+
112+
@Test
113+
fun `token body contains matrix data`() {
114+
val token = createToken()
115+
116+
assertEquals(mutableMapOf("room_id" to "room-id", "server_name" to "matrix-server-name", "token" to "matrix-token"), parseMatrixData(token))
117+
}
118+
119+
@Test
120+
fun `token body contains user data`() {
121+
val token = createToken()
122+
123+
assertEquals(mutableMapOf("name" to "user-display-name", "avatar" to "user-avatar-url"), parseUserData(token))
124+
}
125+
126+
private fun createToken(): String {
127+
return createToken("matrix-token")
128+
}
129+
130+
private fun createToken(accessToken: String): String {
131+
val openIdToken = OpenIdToken(accessToken, "matrix-token-type", "matrix-server-name", -1)
132+
return factory.create(openIdToken, "jitsi-server-domain", "room-id", "user-avatar-url", "user-display-name")
133+
}
134+
135+
private fun parseTokenHeader(token: String): Map<String, String> {
136+
return parseTokenPart(token.split(".")[0], stringToString)
137+
}
138+
139+
private fun parseTokenBody(token: String): Map<String, Any> {
140+
return parseTokenPart(token.split(".")[1], stringToAny)
141+
}
142+
143+
private fun parseMatrixData(token: String): Map<*, *> {
144+
return (parseTokenBody(token)["context"] as Map<*, *>)["matrix"] as Map<*, *>
145+
}
146+
147+
private fun parseUserData(token: String): Map<*, *> {
148+
return (parseTokenBody(token)["context"] as Map<*, *>)["user"] as Map<*, *>
149+
}
150+
151+
private fun <T> parseTokenPart(value: String, type: ParameterizedType): T {
152+
val decoded = String(base64Decoder.decode(value))
153+
val adapter: JsonAdapter<T> = moshi.adapter(type)
154+
return adapter.fromJson(decoded)!!
155+
}
156+
}

0 commit comments

Comments
 (0)