Fixes #7758: Fixed JWT token for Jitsi openidtoken-jwt authentication
Signed-off-by: Alexey Nechaev <seysane@yahoo.com>
This commit is contained in:
parent
40bbd3ebd1
commit
28da02c583
|
@ -0,0 +1 @@
|
|||
Fixed JWT token for Jitsi openidtoken-jwt authentication
|
|
@ -282,6 +282,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
|
||||
|
|
|
@ -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() {
|
||||
|
@ -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,
|
||||
|
@ -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())
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)!!
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue