Skip to content

Commit

Permalink
Merge pull request #8341 from tomtit/bugfix/issue-7758
Browse files Browse the repository at this point in the history
Fixes #7758: Fixed JWT token for Jitsi openidtoken-jwt authentication
  • Loading branch information
bmarty authored Aug 28, 2023
2 parents 0a6dbeb + 28da02c commit a3be028
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 2 deletions.
1 change: 1 addition & 0 deletions changelog.d/7758.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed JWT token for Jitsi openidtoken-jwt authentication
1 change: 1 addition & 0 deletions vector/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ dependencies {
runtimeOnly(libs.jsonwebtoken.jjwtOrgjson) {
exclude group: 'org.json', module: 'json' //provided by Android natively
}
testImplementation(libs.jsonwebtoken.jjwtOrgjson)
implementation 'commons-codec:commons-codec:1.15'

// MapTiler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ package im.vector.app.features.call.conference.jwt
import im.vector.app.core.utils.ensureProtocol
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.security.Keys
import org.matrix.android.sdk.api.session.openid.OpenIdToken
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import javax.inject.Inject

class JitsiJWTFactory @Inject constructor() {
Expand All @@ -37,7 +40,12 @@ class JitsiJWTFactory @Inject constructor() {
userDisplayName: String
): String {
// The secret key here is irrelevant, we're only using the JWT to transport data to Prosody in the Jitsi stack.
val key = Keys.secretKeyFor(SignatureAlgorithm.HS256)
// In the PR https://github.com/jitsi/luajwtjitsi/pull/3 the function `luajwtjitsi.decode` was removed and
// we cannot use random secret keys anymore. But the JWT library `jjwt` doesn't accept the hardcoded key `notused`
// from the module `prosody-mod-auth-matrix-user-verification` since it's too short and thus insecure. So, we
// create a new token using a random key and then re-sign the token manually with the 'weak' key.
val signatureAlgorithm = SignatureAlgorithm.HS256
val key = Keys.secretKeyFor(signatureAlgorithm)
val context = mapOf(
"matrix" to mapOf(
"token" to openIdToken.accessToken,
Expand All @@ -52,7 +60,8 @@ class JitsiJWTFactory @Inject constructor() {
// As per Jitsi token auth, `iss` needs to be set to something agreed between
// JWT generating side and Prosody config. Since we have no configuration for
// the widgets, we can't set one anywhere. Using the Jitsi domain here probably makes sense.
return Jwts.builder()
val token = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setIssuer(jitsiServerDomain)
.setSubject(jitsiServerDomain)
.setAudience(jitsiServerDomain.ensureProtocol())
Expand All @@ -61,5 +70,11 @@ class JitsiJWTFactory @Inject constructor() {
.claim("context", context)
.signWith(key)
.compact()
// Re-sign token with the hardcoded key
val toSign = token.substring(0, token.lastIndexOf('.'))
val mac = Mac.getInstance(signatureAlgorithm.jcaName)
mac.init(SecretKeySpec("notused".toByteArray(), mac.algorithm))
val prosodySignature = Encoders.BASE64URL.encode(mac.doFinal(toSign.toByteArray()))
return "$toSign.$prosodySignature"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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 im.vector.app.features.call.conference.jwt

import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.openid.OpenIdToken
import java.lang.reflect.ParameterizedType
import java.util.Base64
import kotlin.streams.toList

class JitsiJWTFactoryTest {
private val base64Decoder = Base64.getUrlDecoder()
private val moshi = Moshi.Builder().build()
private val stringToString = Types.newParameterizedType(Map::class.java, String::class.java, String::class.java)
private val stringToAny = Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java)
private lateinit var factory: JitsiJWTFactory

@Before
fun init() {
factory = JitsiJWTFactory()
}

@Test
fun `token contains 3 encoded parts`() {
val token = createToken()

val parts = token.split(".")
assertEquals(3, parts.size)
parts.forEach {
assertTrue("Non-empty array", base64Decoder.decode(it).isNotEmpty())
}
}

@Test
fun `token contains unique signature`() {
val signatures = listOf("one", "two").stream()
.map { createToken(it) }
.map { it.split(".")[2] }
.map { base64Decoder.decode(it) }
.toList()

assertEquals(2, signatures.size)
signatures.forEach {
assertEquals(32, it.size)
}
assertFalse("Unique", signatures[0].contentEquals(signatures[1]))
}

@Test
fun `token header contains algorithm`() {
val token = createToken()

assertEquals("HS256", parseTokenHeader(token)["alg"])
}

@Test
fun `token header contains type`() {
val token = createToken()

assertEquals("JWT", parseTokenHeader(token)["typ"])
}

@Test
fun `token body contains subject`() {
val token = createToken()

assertEquals("jitsi-server-domain", parseTokenBody(token)["sub"])
}

@Test
fun `token body contains issuer`() {
val token = createToken()

assertEquals("jitsi-server-domain", parseTokenBody(token)["iss"])
}

@Test
fun `token body contains audience`() {
val token = createToken()

assertEquals("https://jitsi-server-domain", parseTokenBody(token)["aud"])
}

@Test
fun `token body contains room claim`() {
val token = createToken()

assertEquals("*", parseTokenBody(token)["room"])
}

@Test
fun `token body contains matrix data`() {
val token = createToken()

assertEquals(mutableMapOf("room_id" to "room-id", "server_name" to "matrix-server-name", "token" to "matrix-token"), parseMatrixData(token))
}

@Test
fun `token body contains user data`() {
val token = createToken()

assertEquals(mutableMapOf("name" to "user-display-name", "avatar" to "user-avatar-url"), parseUserData(token))
}

private fun createToken(): String {
return createToken("matrix-token")
}

private fun createToken(accessToken: String): String {
val openIdToken = OpenIdToken(accessToken, "matrix-token-type", "matrix-server-name", -1)
return factory.create(openIdToken, "jitsi-server-domain", "room-id", "user-avatar-url", "user-display-name")
}

private fun parseTokenHeader(token: String): Map<String, String> {
return parseTokenPart(token.split(".")[0], stringToString)
}

private fun parseTokenBody(token: String): Map<String, Any> {
return parseTokenPart(token.split(".")[1], stringToAny)
}

private fun parseMatrixData(token: String): Map<*, *> {
return (parseTokenBody(token)["context"] as Map<*, *>)["matrix"] as Map<*, *>
}

private fun parseUserData(token: String): Map<*, *> {
return (parseTokenBody(token)["context"] as Map<*, *>)["user"] as Map<*, *>
}

private fun <T> parseTokenPart(value: String, type: ParameterizedType): T {
val decoded = String(base64Decoder.decode(value))
val adapter: JsonAdapter<T> = moshi.adapter(type)
return adapter.fromJson(decoded)!!
}
}

0 comments on commit a3be028

Please sign in to comment.