mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-16 12:00:03 +03:00
Jitsi auth: introduce JitsiService and JWT token creation
This commit is contained in:
parent
1a70fa0fcc
commit
82c50b7c1d
6 changed files with 291 additions and 3 deletions
|
@ -303,6 +303,7 @@ dependencies {
|
||||||
def arch_version = '2.1.0'
|
def arch_version = '2.1.0'
|
||||||
def lifecycle_version = '2.2.0'
|
def lifecycle_version = '2.2.0'
|
||||||
def rxbinding_version = '3.1.0'
|
def rxbinding_version = '3.1.0'
|
||||||
|
def jjwt_version = '0.11.2'
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
def kluent_version = '1.65'
|
def kluent_version = '1.65'
|
||||||
|
@ -444,9 +445,9 @@ dependencies {
|
||||||
|
|
||||||
// Jitsi
|
// Jitsi
|
||||||
implementation('org.jitsi.react:jitsi-meet-sdk:3.1.0') {
|
implementation('org.jitsi.react:jitsi-meet-sdk:3.1.0') {
|
||||||
exclude group: 'com.google.firebase'
|
exclude group: 'com.google.firebase'
|
||||||
exclude group: 'com.google.android.gms'
|
exclude group: 'com.google.android.gms'
|
||||||
exclude group: 'com.android.installreferrer'
|
exclude group: 'com.android.installreferrer'
|
||||||
}
|
}
|
||||||
|
|
||||||
// QR-code
|
// QR-code
|
||||||
|
@ -460,6 +461,15 @@ dependencies {
|
||||||
|
|
||||||
implementation 'im.dlg:android-dialer:1.2.5'
|
implementation 'im.dlg:android-dialer:1.2.5'
|
||||||
|
|
||||||
|
// JWT
|
||||||
|
api "io.jsonwebtoken:jjwt-api:$jjwt_version"
|
||||||
|
runtimeOnly "io.jsonwebtoken:jjwt-impl:$jjwt_version"
|
||||||
|
runtimeOnly("io.jsonwebtoken:jjwt-orgjson:$jjwt_version") {
|
||||||
|
exclude group: 'org.json', module: 'json' //provided by Android natively
|
||||||
|
}
|
||||||
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
|
|
||||||
|
|
||||||
// TESTS
|
// TESTS
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation "org.amshove.kluent:kluent-android:$kluent_version"
|
testImplementation "org.amshove.kluent:kluent-android:$kluent_version"
|
||||||
|
|
47
vector/src/main/java/im/vector/app/core/network/OkHttp.kt
Normal file
47
vector/src/main/java/im/vector/app/core/network/OkHttp.kt
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.core.network
|
||||||
|
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
|
import okhttp3.Response
|
||||||
|
import java.io.IOException
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
|
suspend fun Call.await(): Response {
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
enqueue(object : Callback {
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
continuation.resume(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
if (continuation.isCancelled) return
|
||||||
|
continuation.resumeWithException(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
try {
|
||||||
|
cancel()
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
//Ignore cancel exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
vector/src/main/java/im/vector/app/core/utils/Base32.kt
Normal file
28
vector/src/main/java/im/vector/app/core/utils/Base32.kt
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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.core.utils
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Base32
|
||||||
|
|
||||||
|
fun String.toBase32String(padding: Boolean = true): String {
|
||||||
|
val base32 = Base32().encodeAsString(toByteArray())
|
||||||
|
return if (padding) {
|
||||||
|
base32
|
||||||
|
} else {
|
||||||
|
base32.replace("=", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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
|
||||||
|
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.network.await
|
||||||
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.core.utils.toBase32String
|
||||||
|
import im.vector.app.features.raw.wellknown.getElementWellknown
|
||||||
|
import im.vector.app.features.settings.VectorLocale
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
import org.matrix.android.sdk.api.raw.RawService
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||||
|
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||||
|
import org.matrix.android.sdk.api.util.appendParamToUrl
|
||||||
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class JitsiService @Inject constructor(
|
||||||
|
private val session: Session,
|
||||||
|
private val rawService: RawService,
|
||||||
|
private val stringProvider: StringProvider) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val JITSI_OPEN_ID_TOKEN_JWT_AUTH = "openidtoken-jwt"
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createJitsiWidget(roomId: String, withVideo: Boolean): Widget {
|
||||||
|
// Build data for a jitsi widget
|
||||||
|
val widgetId: String = WidgetType.Jitsi.preferred + "_" + session.myUserId + "_" + System.currentTimeMillis()
|
||||||
|
val preferredJitsiDomain = tryOrNull {
|
||||||
|
rawService.getElementWellknown(session.myUserId)
|
||||||
|
?.jitsiServer
|
||||||
|
?.preferredDomain
|
||||||
|
}
|
||||||
|
val jitsiDomain = preferredJitsiDomain ?: stringProvider.getString(R.string.preferred_jitsi_domain)
|
||||||
|
val jitsiAuth = getJitsiAuth(jitsiDomain)
|
||||||
|
val confId = createConferenceId(roomId, jitsiAuth)
|
||||||
|
|
||||||
|
// We use the default element wrapper for this widget
|
||||||
|
// https://github.com/vector-im/element-web/blob/develop/docs/jitsi-dev.md
|
||||||
|
// https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/utils/WidgetUtils.ts#L469
|
||||||
|
val url = buildString {
|
||||||
|
append("https://app.element.io/jitsi.html")
|
||||||
|
appendParamToUrl("confId", confId)
|
||||||
|
append("#conferenceDomain=\$domain")
|
||||||
|
append("&conferenceId=\$conferenceId")
|
||||||
|
append("&isAudioOnly=\$isAudioOnly")
|
||||||
|
append("&displayName=\$matrix_display_name")
|
||||||
|
append("&avatarUrl=\$matrix_avatar_url")
|
||||||
|
append("&userId=\$matrix_user_id")
|
||||||
|
append("&roomId=\$matrix_room_id")
|
||||||
|
append("&theme=\$theme")
|
||||||
|
if (jitsiAuth != null) {
|
||||||
|
append("&auth=$jitsiAuth")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val widgetEventContent = mapOf(
|
||||||
|
"url" to url,
|
||||||
|
"type" to WidgetType.Jitsi.legacy,
|
||||||
|
"data" to mapOf(
|
||||||
|
"conferenceId" to confId,
|
||||||
|
"domain" to jitsiDomain,
|
||||||
|
"isAudioOnly" to !withVideo,
|
||||||
|
"authenticationType" to jitsiAuth
|
||||||
|
),
|
||||||
|
"creatorUserId" to session.myUserId,
|
||||||
|
"id" to widgetId,
|
||||||
|
"name" to "jitsi"
|
||||||
|
)
|
||||||
|
|
||||||
|
return session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createConferenceId(roomId: String, jitsiAuth: String?): String {
|
||||||
|
return if (jitsiAuth == JITSI_OPEN_ID_TOKEN_JWT_AUTH) {
|
||||||
|
// Create conference ID from room ID
|
||||||
|
// For compatibility with Jitsi, use base32 without padding.
|
||||||
|
// More details here:
|
||||||
|
// https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
|
||||||
|
roomId.toBase32String(padding = false)
|
||||||
|
} else {
|
||||||
|
// Create a random conference ID
|
||||||
|
// Create a random enough jitsi conference id
|
||||||
|
// Note: the jitsi server automatically creates conference when the conference
|
||||||
|
// id does not exist yet
|
||||||
|
var widgetSessionId = UUID.randomUUID().toString()
|
||||||
|
if (widgetSessionId.length > 8) {
|
||||||
|
widgetSessionId = widgetSessionId.substring(0, 7)
|
||||||
|
}
|
||||||
|
roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.lowercase(VectorLocale.applicationLocale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getJitsiAuth(jitsiDomain: String): String? {
|
||||||
|
val request = Request.Builder().url("https://$jitsiDomain/.well-known/element/jitsi").build()
|
||||||
|
return tryOrNull {
|
||||||
|
val response = session.getOkHttpClient().newCall(request).await()
|
||||||
|
val json = response.body?.string() ?: return null
|
||||||
|
MoshiProvider.providesMoshi().adapter(JitsiWellKnown::class.java).fromJson(json)?.auth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class JitsiWellKnown(
|
||||||
|
@Json(name = "auth") val auth: String
|
||||||
|
)
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 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 io.jsonwebtoken.Jwts
|
||||||
|
import io.jsonwebtoken.SignatureAlgorithm
|
||||||
|
import io.jsonwebtoken.security.Keys
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class JitsiJWTFactory @Inject constructor() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a JWT token for jitsi openidtoken-jwt authentication
|
||||||
|
* See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
|
||||||
|
*/
|
||||||
|
fun create(jitsiServerDomain: String,
|
||||||
|
openIdAccessToken: String,
|
||||||
|
roomId: String,
|
||||||
|
userAvatarUrl: String,
|
||||||
|
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)
|
||||||
|
val context = mapOf(
|
||||||
|
"user" to mapOf(
|
||||||
|
"name" to userDisplayName,
|
||||||
|
"avatar" to userAvatarUrl
|
||||||
|
),
|
||||||
|
"matrix" to mapOf(
|
||||||
|
"token" to openIdAccessToken,
|
||||||
|
"room_id" to roomId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return Jwts.builder()
|
||||||
|
.setIssuer(jitsiServerDomain)
|
||||||
|
.setSubject(jitsiServerDomain)
|
||||||
|
.setAudience("https://$jitsiServerDomain")
|
||||||
|
// room is not used at the moment, a * works here.
|
||||||
|
.claim("room", "*")
|
||||||
|
.claim("context", context)
|
||||||
|
.signWith(key)
|
||||||
|
.compact()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue