Skip to content

Commit

Permalink
Merge pull request #7920 from vector-im/hughns/msc3824-oidc-aware
Browse files Browse the repository at this point in the history
Implementation of MSC3824 to make the client OIDC-aware
  • Loading branch information
bmarty authored Feb 9, 2023
2 parents f4367a0 + b1d7831 commit 4cc2daa
Show file tree
Hide file tree
Showing 40 changed files with 303 additions and 64 deletions.
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 @@ -1063,6 +1063,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">Your account details are managed separately 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 @@ -80,6 +80,11 @@ data class HomeServerCapabilities(
* True if the home server supports event redaction with relations.
*/
var canRedactEventWithRelations: 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? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo047
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo048
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo049
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo050
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo051
import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject
Expand All @@ -75,7 +76,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer
) : MatrixRealmMigration(
dbName = "Session",
schemaVersion = 50L,
schemaVersion = 51L,
) {
/**
* Forces all RealmSessionStoreMigration instances to be equal.
Expand Down Expand Up @@ -135,5 +136,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 48) MigrateSessionTo048(realm).perform()
if (oldVersion < 49) MigrateSessionTo049(realm).perform()
if (oldVersion < 50) MigrateSessionTo050(realm).perform()
if (oldVersion < 51) MigrateSessionTo051(realm).perform()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ internal object HomeServerCapabilitiesMapper {
canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications,
canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices,
canRedactEventWithRelations = entity.canRedactEventWithRelations,
externalAccountManagementUrl = entity.externalAccountManagementUrl,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 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.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 MigrateSessionTo051(realm: DynamicRealm) : RealmMigrator(realm, 51) {

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 @@ -35,6 +35,7 @@ internal open class HomeServerCapabilitiesEntity(
var canUseThreadReadReceiptsAndNotifications: Boolean = false,
var canRemotelyTogglePushNotificationsOfDevices: Boolean = false,
var canRedactEventWithRelations: Boolean = false,
var externalAccountManagementUrl: String? = null,
) : RealmObject() {

companion object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,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

0 comments on commit 4cc2daa

Please sign in to comment.