mirror of
https://github.com/element-hq/element-android
synced 2024-11-23 18:05:36 +03:00
Merge pull request #8341 from tomtit/bugfix/issue-7758
Fixes #7758: Fixed JWT token for Jitsi openidtoken-jwt authentication
This commit is contained in:
commit
a3be0286ee
4 changed files with 175 additions and 2 deletions
1
changelog.d/7758.bugfix
Normal file
1
changelog.d/7758.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fixed JWT token for Jitsi openidtoken-jwt authentication
|
|
@ -277,6 +277,7 @@ dependencies {
|
||||||
runtimeOnly(libs.jsonwebtoken.jjwtOrgjson) {
|
runtimeOnly(libs.jsonwebtoken.jjwtOrgjson) {
|
||||||
exclude group: 'org.json', module: 'json' //provided by Android natively
|
exclude group: 'org.json', module: 'json' //provided by Android natively
|
||||||
}
|
}
|
||||||
|
testImplementation(libs.jsonwebtoken.jjwtOrgjson)
|
||||||
implementation 'commons-codec:commons-codec:1.15'
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
|
|
||||||
// MapTiler
|
// MapTiler
|
||||||
|
|
|
@ -19,8 +19,11 @@ package im.vector.app.features.call.conference.jwt
|
||||||
import im.vector.app.core.utils.ensureProtocol
|
import im.vector.app.core.utils.ensureProtocol
|
||||||
import io.jsonwebtoken.Jwts
|
import io.jsonwebtoken.Jwts
|
||||||
import io.jsonwebtoken.SignatureAlgorithm
|
import io.jsonwebtoken.SignatureAlgorithm
|
||||||
|
import io.jsonwebtoken.io.Encoders
|
||||||
import io.jsonwebtoken.security.Keys
|
import io.jsonwebtoken.security.Keys
|
||||||
import org.matrix.android.sdk.api.session.openid.OpenIdToken
|
import org.matrix.android.sdk.api.session.openid.OpenIdToken
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class JitsiJWTFactory @Inject constructor() {
|
class JitsiJWTFactory @Inject constructor() {
|
||||||
|
@ -37,7 +40,12 @@ class JitsiJWTFactory @Inject constructor() {
|
||||||
userDisplayName: String
|
userDisplayName: String
|
||||||
): String {
|
): String {
|
||||||
// The secret key here is irrelevant, we're only using the JWT to transport data to Prosody in the Jitsi stack.
|
// 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(
|
val context = mapOf(
|
||||||
"matrix" to mapOf(
|
"matrix" to mapOf(
|
||||||
"token" to openIdToken.accessToken,
|
"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
|
// 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
|
// 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.
|
// 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)
|
.setIssuer(jitsiServerDomain)
|
||||||
.setSubject(jitsiServerDomain)
|
.setSubject(jitsiServerDomain)
|
||||||
.setAudience(jitsiServerDomain.ensureProtocol())
|
.setAudience(jitsiServerDomain.ensureProtocol())
|
||||||
|
@ -61,5 +70,11 @@ class JitsiJWTFactory @Inject constructor() {
|
||||||
.claim("context", context)
|
.claim("context", context)
|
||||||
.signWith(key)
|
.signWith(key)
|
||||||
.compact()
|
.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 a new issue