From 507fb8a3ce6024c35a58381ccee0de9849c3e23c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 30 Apr 2024 11:24:11 +0200 Subject: [PATCH] Reworked auth requested callback & SSO activity to handle Bearer authentication requests --- app/src/main/AndroidManifest.xml | 15 --- .../java/org/linphone/core/CoreContext.kt | 78 +++++++++++++ .../java/org/linphone/ui/main/MainActivity.kt | 33 ++++-- .../ui/main/recordings/RecordingsFragment.kt | 1 + .../sso/fragment/SingleSignOnFragment.kt} | 79 +++++++------ .../sso}/viewmodel/SingleSignOnViewModel.kt | 63 ++++++++--- .../ui/main/viewmodel/MainViewModel.kt | 45 -------- .../res/layout/account_profile_fragment.xml | 5 +- .../assistant_single_sign_on_activity.xml | 2 +- .../res/layout/single_sign_on_fragment.xml | 106 ++++++++++++++++++ .../main/res/navigation/main_nav_graph.xml | 21 ++++ 11 files changed, 324 insertions(+), 124 deletions(-) rename app/src/main/java/org/linphone/ui/{assistant/SingleSignOnActivity.kt => main/sso/fragment/SingleSignOnFragment.kt} (52%) rename app/src/main/java/org/linphone/ui/{assistant => main/sso}/viewmodel/SingleSignOnViewModel.kt (84%) create mode 100644 app/src/main/res/layout/single_sign_on_fragment.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bd21fcd50..5ed8e2571 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -114,21 +114,6 @@ android:launchMode="singleTask" android:resizeableActivity="true" /> - - - - - - - - - - - >> by lazy { + MutableLiveData>>() + } + + val digestAuthenticationRequestedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val greenToastToShowEvent: MutableLiveData>> by lazy { MutableLiveData>>() } @@ -210,6 +221,57 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C } } } + + @WorkerThread + override fun onAuthenticationRequested(core: Core, authInfo: AuthInfo, method: AuthMethod) { + if (authInfo.username == null || authInfo.domain == null || authInfo.realm == null) { + Log.e( + "$TAG Authentication request but either username [${authInfo.username}], domain [${authInfo.domain}] or realm [${authInfo.realm}] is null!" + ) + return + } + + when (method) { + AuthMethod.Bearer -> { + val serverUrl = authInfo.authorizationServer + val username = authInfo.username + if (!serverUrl.isNullOrEmpty()) { + Log.i( + "$TAG Authentication requested method is Bearer, starting Single Sign On activity with server URL [$serverUrl] and username [$username]" + ) + bearerAuthInfoPendingPasswordUpdate = authInfo + bearerAuthenticationRequestedEvent.postValue( + Event(Pair(serverUrl, username)) + ) + } else { + Log.e( + "$TAG Authentication requested method is Bearer but no authorization server was found in auth info!" + ) + } + } + AuthMethod.HttpDigest -> { + val accountFound = core.accountList.find { + it.params.identityAddress?.username == authInfo.username && it.params.identityAddress?.domain == authInfo.domain + } + if (accountFound == null) { + Log.w( + "$TAG Failed to find account matching auth info, aborting auth dialog" + ) + return + } + + val identity = "${authInfo.username}@${authInfo.domain}" + Log.i( + "$TAG Authentication requested method is HttpDigest, showing dialog asking user for password for identity [$identity]" + ) + digestAuthInfoPendingPasswordUpdate = authInfo + digestAuthenticationRequestedEvent.postValue(Event(identity)) + } + AuthMethod.Tls -> { + Log.w("$TAG Authentication requested method is TLS, not doing anything...") + } + } + } } private val loggingServiceListener = object : LoggingServiceListenerStub() { @@ -392,6 +454,22 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C } } + @WorkerThread + fun updateAuthInfo(password: String) { + val authInfo = digestAuthInfoPendingPasswordUpdate + if (authInfo != null) { + Log.i( + "$TAG Updating password for username [${authInfo.username}] using auth info [$authInfo]" + ) + authInfo.password = password + core.addAuthInfo(authInfo) + digestAuthInfoPendingPasswordUpdate = null + core.refreshRegisters() + } else { + Log.e("$TAG No pending auth info for digest authentication!") + } + } + @WorkerThread fun isAddressMyself(address: Address): Boolean { val found = core.accountList.find { diff --git a/app/src/main/java/org/linphone/ui/main/MainActivity.kt b/app/src/main/java/org/linphone/ui/main/MainActivity.kt index 0eea7aaeb..961498c60 100644 --- a/app/src/main/java/org/linphone/ui/main/MainActivity.kt +++ b/app/src/main/java/org/linphone/ui/main/MainActivity.kt @@ -53,6 +53,7 @@ import org.linphone.ui.GenericActivity import org.linphone.ui.assistant.AssistantActivity import org.linphone.ui.main.chat.fragment.ConversationsListFragmentDirections import org.linphone.ui.main.fragment.AuthRequestedDialogModel +import org.linphone.ui.main.sso.fragment.SingleSignOnFragmentDirections import org.linphone.ui.main.viewmodel.MainViewModel import org.linphone.ui.main.viewmodel.SharedMainViewModel import org.linphone.ui.welcome.WelcomeActivity @@ -164,12 +165,6 @@ class MainActivity : GenericActivity() { } } - viewModel.authenticationRequestedEvent.observe(this) { - it.consume { identity -> - showAuthenticationRequestedDialog(identity) - } - } - binding.root.doOnAttach { Log.i("$TAG Report UI has been fully drawn (TTFD)") try { @@ -179,6 +174,28 @@ class MainActivity : GenericActivity() { } } + coreContext.bearerAuthenticationRequestedEvent.observe(this) { + it.consume { pair -> + val serverUrl = pair.first + val username = pair.second + + Log.i( + "$TAG Navigating to Single Sign On Fragment with server URL [$serverUrl] and username [$username]" + ) + val action = SingleSignOnFragmentDirections.actionGlobalSingleSignOnFragment( + serverUrl, + username + ) + findNavController().navigate(action) + } + } + + coreContext.digestAuthenticationRequestedEvent.observe(this) { + it.consume { identity -> + showAuthenticationRequestedDialog(identity) + } + } + coreContext.greenToastToShowEvent.observe(this) { it.consume { pair -> val message = pair.first @@ -586,7 +603,9 @@ class MainActivity : GenericActivity() { model.confirmEvent.observe(this) { it.consume { password -> - viewModel.updateAuthInfo(password) + coreContext.postOnCoreThread { + coreContext.updateAuthInfo(password) + } dialog.dismiss() } } diff --git a/app/src/main/java/org/linphone/ui/main/recordings/RecordingsFragment.kt b/app/src/main/java/org/linphone/ui/main/recordings/RecordingsFragment.kt index d4c97ed2c..aabcd5e4d 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/RecordingsFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/RecordingsFragment.kt @@ -30,6 +30,7 @@ import org.linphone.ui.main.fragment.GenericFragment @UiThread class RecordingsFragment : GenericFragment() { private lateinit var binding: RecordingsFragmentBinding + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/app/src/main/java/org/linphone/ui/assistant/SingleSignOnActivity.kt b/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt similarity index 52% rename from app/src/main/java/org/linphone/ui/assistant/SingleSignOnActivity.kt rename to app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt index 39a5e3c70..95f0354af 100644 --- a/app/src/main/java/org/linphone/ui/assistant/SingleSignOnActivity.kt +++ b/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt @@ -17,83 +17,80 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.ui.assistant +package org.linphone.ui.main.sso.fragment import android.content.Intent import android.os.Bundle -import androidx.annotation.UiThread -import androidx.databinding.DataBindingUtil -import androidx.lifecycle.ViewModelProvider +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.navArgs +import androidx.navigation.navGraphViewModels import net.openid.appauth.AuthorizationException import net.openid.appauth.AuthorizationResponse import org.linphone.R import org.linphone.core.tools.Log -import org.linphone.databinding.AssistantSingleSignOnActivityBinding +import org.linphone.databinding.SingleSignOnFragmentBinding import org.linphone.ui.GenericActivity -import org.linphone.ui.assistant.viewmodel.SingleSignOnViewModel +import org.linphone.ui.main.fragment.GenericFragment +import org.linphone.ui.main.sso.viewmodel.SingleSignOnViewModel -@UiThread -class SingleSignOnActivity : GenericActivity() { +class SingleSignOnFragment : GenericFragment() { companion object { - private const val TAG = "[Single Sign On Activity]" + private const val TAG = "[Single Sign On Fragment]" private const val ACTIVITY_RESULT_ID = 666 } - private lateinit var binding: AssistantSingleSignOnActivityBinding + private lateinit var binding: SingleSignOnFragmentBinding - private lateinit var viewModel: SingleSignOnViewModel + private val viewModel: SingleSignOnViewModel by navGraphViewModels( + R.id.main_nav_graph + ) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + private val args: SingleSignOnFragmentArgs by navArgs() - binding = DataBindingUtil.setContentView(this, R.layout.assistant_single_sign_on_activity) - binding.lifecycleOwner = this + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SingleSignOnFragmentBinding.inflate(layoutInflater) + return binding.root + } - viewModel = ViewModelProvider(this)[SingleSignOnViewModel::class.java] + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.lifecycleOwner = viewLifecycleOwner binding.viewModel = viewModel - setUpToastsArea(binding.toastsArea) - - if (intent != null) { - Log.i( - "$TAG Handling intent action [${intent.action}], type [${intent.type}] and data [${intent.data}]" - ) - val uri = intent.data?.toString() ?: "" - if (uri.startsWith("linphone-sso:")) { - val ssoUrl = uri.replace("linphone-sso:", "https:") - Log.i("$TAG Setting SSO URL [$ssoUrl]") - viewModel.singleSignOnUrl.value = ssoUrl - } - } - - viewModel.singleSignOnUrl.observe(this) { url -> - Log.i("$TAG SSO URL found [$url], setting it up") - viewModel.setUp() - } - - viewModel.singleSignOnProcessCompletedEvent.observe(this) { + viewModel.singleSignOnProcessCompletedEvent.observe(viewLifecycleOwner) { it.consume { - Log.i("$TAG Process complete, leaving assistant") - finish() + Log.i("$TAG Process complete, going back") + goBack() } } - viewModel.startAuthIntentEvent.observe(this) { + viewModel.startAuthIntentEvent.observe(viewLifecycleOwner) { it.consume { intent -> Log.i("$TAG Starting auth intent activity") startActivityForResult(intent, ACTIVITY_RESULT_ID) } } - viewModel.onErrorEvent.observe(this) { + viewModel.onErrorEvent.observe(viewLifecycleOwner) { it.consume { errorMessage -> - showRedToast( + (requireActivity() as GenericActivity).showRedToast( errorMessage, R.drawable.warning_circle ) } } + + val serverUrl = args.serverUrl + val username = args.username + Log.i("$TAG Found server URL [$serverUrl] and username [$username] in args") + viewModel.setUp(serverUrl, username.orEmpty()) } @Deprecated("Deprecated in Java") diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/SingleSignOnViewModel.kt b/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt similarity index 84% rename from app/src/main/java/org/linphone/ui/assistant/viewmodel/SingleSignOnViewModel.kt rename to app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt index 043aabe89..b838bff0f 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/SingleSignOnViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.ui.assistant.viewmodel +package org.linphone.ui.main.sso.viewmodel import android.content.Intent import android.net.Uri @@ -35,6 +35,7 @@ import net.openid.appauth.AuthorizationService import net.openid.appauth.AuthorizationServiceConfiguration import net.openid.appauth.ResponseTypeValues import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Factory import org.linphone.core.tools.Log import org.linphone.utils.Event import org.linphone.utils.FileUtils @@ -50,7 +51,9 @@ class SingleSignOnViewModel : ViewModel() { val singleSignOnProcessCompletedEvent = MutableLiveData>() - val singleSignOnUrl = MutableLiveData() + private var singleSignOnUrl = "" + + private var username: String = "" val startAuthIntentEvent: MutableLiveData> by lazy { MutableLiveData>() @@ -60,15 +63,18 @@ class SingleSignOnViewModel : ViewModel() { MutableLiveData>() } - private var preFilledUser: String = "" - private lateinit var authState: AuthState private lateinit var authService: AuthorizationService @UiThread - fun setUp() { + fun setUp(ssoUrl: String, user: String = "") { viewModelScope.launch { - Log.i("$TAG Setting up SSO environment, redirect URI is [$REDIRECT_URI]") + singleSignOnUrl = ssoUrl + username = user + + Log.i( + "$TAG Setting up SSO environment for username [$username] and URL [$singleSignOnUrl], redirect URI is [$REDIRECT_URI]" + ) authState = getAuthState() updateTokenInfo() } @@ -94,7 +100,7 @@ class SingleSignOnViewModel : ViewModel() { private fun singleSignOn() { Log.i("$TAG Fetch from issuer") AuthorizationServiceConfiguration.fetchFromUrl( - Uri.parse(singleSignOnUrl.value), + Uri.parse(singleSignOnUrl), AuthorizationServiceConfiguration.RetrieveConfigurationCallback { serviceConfiguration, ex -> if (ex != null) { Log.e("$TAG Failed to fetch configuration") @@ -120,8 +126,8 @@ class SingleSignOnViewModel : ViewModel() { Uri.parse(REDIRECT_URI) // the redirect URI to which the auth response is sent ) - if (preFilledUser.isNotEmpty()) { - authRequestBuilder.setLoginHint(preFilledUser) + if (username.isNotEmpty()) { + authRequestBuilder.setLoginHint(username) } val authRequest = authRequestBuilder.build() @@ -187,7 +193,7 @@ class SingleSignOnViewModel : ViewModel() { storeAuthStateAsJsonFile() } - useToken() + storeTokensInAuthInfo() } else { Log.e("$TAG Failed to perform token request [$ex]") onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty())) @@ -214,9 +220,7 @@ class SingleSignOnViewModel : ViewModel() { return@AuthStateAction } - Log.i("$$TAG Access & id tokens are now available") - Log.d("$TAG Access token [$accessToken], id token [$idToken]") - + Log.i("$TAG Access & id tokens are now available") storeAuthStateAsJsonFile() } )*/ @@ -288,7 +292,7 @@ class SingleSignOnViewModel : ViewModel() { } val time = TimestampUtils.toString(expiration, timestampInSecs = false) Log.i("$TAG Access token expires [$date] [$time]") - singleSignOnProcessCompletedEvent.postValue(Event(true)) + storeTokensInAuthInfo() } } else { Log.w("$TAG Access token expiration info not available") @@ -307,4 +311,35 @@ class SingleSignOnViewModel : ViewModel() { singleSignOn() } } + + @UiThread + private fun storeTokensInAuthInfo() { + coreContext.postOnCoreThread { core -> + val expire = authState.accessTokenExpirationTime + if (expire == null) { + Log.e("$TAG Access token expiration time is null!") + onErrorEvent.postValue(Event("Invalid access token expiration time")) + } else { + val accessToken = + Factory.instance().createBearerToken(authState.accessToken, expire) + val refreshToken = + Factory.instance().createBearerToken(authState.refreshToken, expire) + + val authInfo = coreContext.bearerAuthInfoPendingPasswordUpdate + if (authInfo == null) { + Log.e("$TAG No pending auth info in CoreContext!") + return@postOnCoreThread + } + authInfo.accessToken = accessToken + authInfo.refreshToken = refreshToken + core.addAuthInfo(authInfo) + + Log.i( + "$TAG Auth info for username [$username] filled with access token [${authState.accessToken}], refresh token [${authState.refreshToken}] and expire [$expire], refreshing REGISTERs" + ) + core.refreshRegisters() + singleSignOnProcessCompletedEvent.postValue(Event(true)) + } + } + } } diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt index e0d2ba9b7..b422d49b9 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt @@ -34,8 +34,6 @@ import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.core.Account -import org.linphone.core.AuthInfo -import org.linphone.core.AuthMethod import org.linphone.core.Call import org.linphone.core.Core import org.linphone.core.CoreListenerStub @@ -93,10 +91,6 @@ class MainViewModel @UiThread constructor() : ViewModel() { MutableLiveData>() } - val authenticationRequestedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - var accountsFound = -1 var mainIntentHandled = false @@ -109,8 +103,6 @@ class MainViewModel @UiThread constructor() : ViewModel() { private var firstAccountRegistered: Boolean = false - private var authInfoPendingPasswordUpdate: AuthInfo? = null - private val coreListener = object : CoreListenerStub() { @WorkerThread override fun onLastCallEnded(core: Core) { @@ -237,27 +229,6 @@ class MainViewModel @UiThread constructor() : ViewModel() { core.defaultAccount = core.accountList.firstOrNull() } } - - @WorkerThread - override fun onAuthenticationRequested(core: Core, authInfo: AuthInfo, method: AuthMethod) { - if (authInfo.username == null || authInfo.domain == null || authInfo.realm == null) { - return - } - - Log.w( - "$TAG Authentication requested for account [${authInfo.username}@${authInfo.domain}] with realm [${authInfo.realm}] using method [$method]" - ) - val accountFound = core.accountList.find { - it.params.identityAddress?.username == authInfo.username && it.params.identityAddress?.domain == authInfo.domain - } - if (accountFound == null) { - Log.w("$TAG Failed to find account matching auth info, aborting auth dialog") - return - } - val identity = "${authInfo.username}@${authInfo.domain}" - authInfoPendingPasswordUpdate = authInfo - authenticationRequestedEvent.postValue(Event(identity)) - } } init { @@ -340,22 +311,6 @@ class MainViewModel @UiThread constructor() : ViewModel() { } } - @UiThread - fun updateAuthInfo(password: String) { - coreContext.postOnCoreThread { core -> - val authInfo = authInfoPendingPasswordUpdate - if (authInfo != null) { - Log.i( - "$TAG Updating password for username [${authInfo.username}] using auth info [$authInfo]" - ) - authInfo.password = password - core.addAuthInfo(authInfo) - authInfoPendingPasswordUpdate = null - core.refreshRegisters() - } - } - } - @WorkerThread private fun updateCallAlert() { val core = coreContext.core diff --git a/app/src/main/res/layout/account_profile_fragment.xml b/app/src/main/res/layout/account_profile_fragment.xml index e00d1c463..c26fc120d 100644 --- a/app/src/main/res/layout/account_profile_fragment.xml +++ b/app/src/main/res/layout/account_profile_fragment.xml @@ -250,7 +250,7 @@ android:textColor="@color/gray_main2_600" android:maxLines="1" android:background="@drawable/edit_text_background" - android:inputType="text|textPersonName" + android:inputType="text|textPersonName|textCapSentences" app:layout_constraintHorizontal_bias="0" app:layout_constraintWidth_max="@dimen/text_input_max_width" app:layout_constraintTop_toBottomOf="@id/display_name_label" @@ -403,6 +403,7 @@ android:layout_marginEnd="16dp" android:contentDescription="@null" android:src="@drawable/shape_squircle_white_background" + android:visibility="@{viewModel.showModeSelection ? View.VISIBLE : View.GONE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/connection_background" @@ -416,6 +417,7 @@ android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:text="@{viewModel.isCurrentlySelectedModeSecure ? @string/manage_account_e2e_encrypted_mode_default_title : @string/manage_account_e2e_encrypted_mode_interoperable_title, default=@string/manage_account_e2e_encrypted_mode_default_title}" + android:visibility="@{viewModel.showModeSelection ? View.VISIBLE : View.GONE}" app:layout_constraintTop_toTopOf="@id/mode_background" app:layout_constraintStart_toStartOf="@id/mode_background" app:layout_constraintBottom_toBottomOf="@id/mode_background"/> @@ -436,6 +438,7 @@ android:text="@string/manage_account_change_mode" android:maxLines="1" android:ellipsize="end" + android:visibility="@{viewModel.showModeSelection ? View.VISIBLE : View.GONE}" app:layout_constraintEnd_toEndOf="@id/mode_background" app:layout_constraintTop_toTopOf="@id/mode_background" app:layout_constraintBottom_toBottomOf="@id/mode_background"/> diff --git a/app/src/main/res/layout/assistant_single_sign_on_activity.xml b/app/src/main/res/layout/assistant_single_sign_on_activity.xml index 7e00bbc82..3a61d8b79 100644 --- a/app/src/main/res/layout/assistant_single_sign_on_activity.xml +++ b/app/src/main/res/layout/assistant_single_sign_on_activity.xml @@ -6,7 +6,7 @@ + type="org.linphone.ui.main.sso.viewmodel.SingleSignOnViewModel" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml index b396b5ecc..07d72949d 100644 --- a/app/src/main/res/navigation/main_nav_graph.xml +++ b/app/src/main/res/navigation/main_nav_graph.xml @@ -437,4 +437,25 @@ android:label="SettingsAdvancedFragment" tools:layout="@layout/settings_advanced_fragment"/> + + + + + + + \ No newline at end of file