Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of MSC3824 to make the client OIDC-aware #6367

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/6367.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Adds MSC3824 OIDC-awareness when talking to an OIDC-enabled homeservers
3 changes: 3 additions & 0 deletions library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,9 @@
<string name="settings_discovery_category">Discovery</string>
<string name="settings_discovery_manage">Manage your discovery settings.</string>

<string name="settings_external_account_management_title">Account</string>
<string name="settings_external_account_management">Manage your account at %1$s.</string>

<!-- analytics -->
<string name="settings_analytics">Analytics</string>
<string name="settings_opt_in_of_analytics">Send analytics data</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ interface AuthenticationService {
/**
* Get a SSO url.
*/
fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String?
fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?, action: SSOAction): String?

/**
* Get the sign in or sign up fallback URL.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.auth

/**
* See https://github.com/matrix-org/matrix-spec-proposals/pull/3824
*/
enum class SSOAction {
LOGIN,
REGISTER;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2023 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.auth.data

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/**
* https://github.com/matrix-org/matrix-spec-proposals/pull/2965
* <pre>
* {
* "issuer": "https://id.server.org",
* "account": "https://id.server.org/my-account",
* }
* </pre>
* .
*/

@JsonClass(generateAdapter = true)
data class DelegatedAuthConfig(
@Json(name = "issuer")
val issuer: String,

@Json(name = "account")
val accountManagementUrl: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ data class LoginFlowResult(
val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String,
val isOutdatedHomeserver: Boolean,
val hasOidcCompatibilityFlow: Boolean,
val isLogoutDevicesSupported: Boolean,
val isLoginWithQrSupported: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,11 @@ data class WellKnown(
val identityServer: WellKnownBaseConfig? = null,

@Json(name = "m.integrations")
val integrations: JsonDict? = null
val integrations: JsonDict? = null,

/**
* For delegation of auth via OIDC as per [MSC2965](https://github.com/matrix-org/matrix-spec-proposals/pull/2965).
*/
@Json(name = "org.matrix.msc2965.authentication")
val unstableDelegatedAuthConfig: DelegatedAuthConfig? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ data class HomeServerCapabilities(
* True if the home server supports remote toggle of Pusher for a given device.
*/
val canRemotelyTogglePushNotificationsOfDevices: Boolean = false,
/**
* External account management url for use with MSC3824 delegated OIDC, provided in Wellknown.
*/
val externalAccountManagementUrl: String? = null,
) {

enum class RoomCapabilitySupport {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.LoginType
import org.matrix.android.sdk.api.auth.SSOAction
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
Expand Down Expand Up @@ -88,7 +89,7 @@ internal class DefaultAuthenticationService @Inject constructor(
return getLoginFlow(homeServerConnectionConfig)
}

override fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? {
override fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?, action: SSOAction): String? {
val homeServerUrlBase = getHomeServerUrlBase() ?: return null

return buildString {
Expand All @@ -103,6 +104,9 @@ internal class DefaultAuthenticationService @Inject constructor(
// But https://github.com/matrix-org/synapse/issues/5755
appendParamToUrl("device_id", it)
}

// unstable MSC3824 action param
appendParamToUrl("org.matrix.msc3824.action", action.toString())
}
}

Expand Down Expand Up @@ -292,12 +296,18 @@ internal class DefaultAuthenticationService @Inject constructor(
val loginFlowResponse = executeRequest(null) {
authAPI.getLoginFlows()
}

// If an m.login.sso flow is present that is flagged as being for MSC3824 OIDC compatibility then we only return that flow
val oidcCompatibilityFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.sso" && it.delegatedOidcCompatibilty == true }
val flows = if (oidcCompatibilityFlow != null) listOf(oidcCompatibilityFlow) else loginFlowResponse.flows

return LoginFlowResult(
supportedLoginTypes = loginFlowResponse.flows.orEmpty().mapNotNull { it.type },
ssoIdentityProviders = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider,
supportedLoginTypes = flows.orEmpty().mapNotNull { it.type },
ssoIdentityProviders = flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider,
isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(),
homeServerUrl = homeServerUrl,
isOutdatedHomeserver = !versions.isSupportedBySdk(),
hasOidcCompatibilityFlow = oidcCompatibilityFlow != null,
isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices(),
isLoginWithQrSupported = versions.doesServerSupportQrCodeLogin(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ internal data class LoginFlow(
* See MSC #2858
*/
@Json(name = "identity_providers")
val ssoIdentityProvider: List<SsoIdentityProvider>? = null
val ssoIdentityProvider: List<SsoIdentityProvider>? = null,

/**
* Whether this login flow is preferred for OIDC-aware clients.
*
* See [MSC3824](https://github.com/matrix-org/matrix-spec-proposals/pull/3824)
*/
@Json(name = "org.matrix.msc3824.delegated_oidc_compatibility")
val delegatedOidcCompatibilty: Boolean?
)
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo046
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo047
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo048
import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject
Expand All @@ -72,7 +73,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer
) : MatrixRealmMigration(
dbName = "Session",
schemaVersion = 47L,
schemaVersion = 48L,
) {
/**
* Forces all RealmSessionStoreMigration instances to be equal.
Expand Down Expand Up @@ -129,5 +130,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 45) MigrateSessionTo045(realm).perform()
if (oldVersion < 46) MigrateSessionTo046(realm).perform()
if (oldVersion < 47) MigrateSessionTo047(realm).perform()
if (oldVersion < 48) MigrateSessionTo048(realm).perform()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ internal object HomeServerCapabilitiesMapper {
canLoginWithQrCode = entity.canLoginWithQrCode,
canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications,
canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices,
externalAccountManagementUrl = entity.externalAccountManagementUrl,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.database.migration

import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
import org.matrix.android.sdk.internal.util.database.RealmMigrator

internal class MigrateSessionTo048(realm: DynamicRealm) : RealmMigrator(realm, 40) {

override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("HomeServerCapabilitiesEntity")
?.addField(HomeServerCapabilitiesEntityFields.EXTERNAL_ACCOUNT_MANAGEMENT_URL, String::class.java)
?.forceRefreshOfHomeServerCapabilities()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ internal open class HomeServerCapabilitiesEntity(
var canLoginWithQrCode: Boolean = false,
var canUseThreadReadReceiptsAndNotifications: Boolean = false,
var canRemotelyTogglePushNotificationsOfDevices: Boolean = false,
var externalAccountManagementUrl: String? = null,
) : RealmObject() {

companion object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
Timber.v("Extracted integration config : $config")
realm.insertOrUpdate(config)
}
homeServerCapabilitiesEntity.externalAccountManagementUrl = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.accountManagementUrl
}
homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.browser.customtabs.CustomTabsSession
import androidx.viewbinding.ViewBinding
import com.airbnb.mvrx.withState
import im.vector.app.core.utils.openUrlInChromeCustomTab
import org.matrix.android.sdk.api.auth.SSOAction

abstract class AbstractSSOLoginFragment<VB : ViewBinding> : AbstractLoginFragment<VB>() {

Expand Down Expand Up @@ -90,7 +91,8 @@ abstract class AbstractSSOLoginFragment<VB : ViewBinding> : AbstractLoginFragmen
loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = null
providerId = null,
action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN
)
?.let { prefetchUrl(it) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ sealed class LoginAction : VectorViewModelAction {
data class SetupSsoForSessionRecovery(
val homeServerUrl: String,
val deviceId: String,
val ssoIdentityProviders: List<SsoIdentityProvider>?
val ssoIdentityProviders: List<SsoIdentityProvider>?,
val hasOidcCompatibilityFlow: Boolean
) : LoginAction()

data class PostViewEvent(val viewEvent: LoginViewEvents) : LoginAction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import im.vector.app.features.login.terms.LoginTermsFragmentArgument
import im.vector.app.features.onboarding.AuthenticationDescription
import im.vector.app.features.pin.UnlockedActivity
import im.vector.lib.core.utils.compat.getParcelableExtraCompat
import org.matrix.android.sdk.api.auth.SSOAction
import org.matrix.android.sdk.api.auth.registration.FlowResult
import org.matrix.android.sdk.api.auth.registration.Stage
import org.matrix.android.sdk.api.auth.toLocalizedLoginTerms
Expand Down Expand Up @@ -300,6 +301,7 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), UnlockedA
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = null,
action = SSOAction.LOGIN
)?.let { ssoUrl ->
openUrlInChromeCustomTab(this, null, ssoUrl)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.auth.SSOAction
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.isInvalidPassword
Expand Down Expand Up @@ -200,11 +201,12 @@ class LoginFragment :

if (state.loginMode is LoginMode.SsoAndPassword) {
views.loginSocialLoginContainer.isVisible = true
views.loginSocialLoginButtons.render(state.loginMode.ssoState, ssoMode(state)) { provider ->
views.loginSocialLoginButtons.render(state.loginMode, ssoMode(state)) { provider ->
loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = provider?.id
providerId = provider?.id,
action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN
)
?.let { openInCustomTab(it) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ sealed class LoginMode : Parcelable { // Parcelable because persist state

@Parcelize object Unknown : LoginMode()
@Parcelize object Password : LoginMode()
@Parcelize data class Sso(val ssoState: SsoState) : LoginMode()
@Parcelize data class SsoAndPassword(val ssoState: SsoState) : LoginMode()
@Parcelize data class Sso(val ssoState: SsoState, val hasOidcCompatibilityFlow: Boolean) : LoginMode()
@Parcelize data class SsoAndPassword(val ssoState: SsoState, val hasOidcCompatibilityFlow: Boolean) : LoginMode()
@Parcelize object Unsupported : LoginMode()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import im.vector.app.R
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginSignupSigninSelectionBinding
import im.vector.app.features.login.SocialLoginButtonsView.Mode
import org.matrix.android.sdk.api.auth.SSOAction

/**
* In this screen, the user is asked to sign up or to sign in to the homeserver.
Expand Down Expand Up @@ -75,11 +76,12 @@ class LoginSignUpSignInSelectionFragment :
when (state.loginMode) {
is LoginMode.SsoAndPassword -> {
views.loginSignupSigninSignInSocialLoginContainer.isVisible = true
views.loginSignupSigninSocialLoginButtons.render(state.loginMode.ssoState(), Mode.MODE_CONTINUE) { provider ->
views.loginSignupSigninSocialLoginButtons.render(state.loginMode, Mode.MODE_CONTINUE) { provider ->
loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = provider?.id
providerId = provider?.id,
action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN
)
?.let { openInCustomTab(it) }
}
Expand Down Expand Up @@ -111,7 +113,8 @@ class LoginSignUpSignInSelectionFragment :
loginViewModel.getSsoUrl(
redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = null
providerId = null,
action = if (state.signMode == SignMode.SignUp) SSOAction.REGISTER else SSOAction.LOGIN
)
?.let { openInCustomTab(it) }
} else {
Expand Down
Loading