Updated third party SIP account login to allow for SIP identity with a different domain for proxy + bearer auth

This commit is contained in:
Sylvain Berfini 2024-05-10 21:38:53 +02:00
parent 655cc8c291
commit ecad8fbdce
8 changed files with 113 additions and 36 deletions

View file

@ -38,6 +38,7 @@ import org.linphone.databinding.AssistantThirdPartySipAccountLoginFragmentBindin
import org.linphone.ui.GenericActivity
import org.linphone.ui.GenericFragment
import org.linphone.ui.assistant.viewmodel.ThirdPartySipAccountLoginViewModel
import org.linphone.ui.main.sso.fragment.SingleSignOnFragmentDirections
import org.linphone.utils.PhoneNumberUtils
@UiThread
@ -119,6 +120,22 @@ class ThirdPartySipAccountLoginFragment : GenericFragment() {
}
}
coreContext.bearerAuthenticationRequestedEvent.observe(viewLifecycleOwner) {
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.postOnCoreThread {
val dialPlan = PhoneNumberUtils.getDeviceDialPlan(requireContext())
if (dialPlan != null) {

View file

@ -124,9 +124,6 @@ class ThirdPartySipAccountLoginViewModel @UiThread constructor() : GenericViewMo
loginEnabled.addSource(username) {
loginEnabled.value = isLoginButtonEnabled()
}
loginEnabled.addSource(password) {
loginEnabled.value = isLoginButtonEnabled()
}
loginEnabled.addSource(domain) {
loginEnabled.value = isLoginButtonEnabled()
}
@ -143,8 +140,31 @@ class ThirdPartySipAccountLoginViewModel @UiThread constructor() : GenericViewMo
coreContext.postOnCoreThread { core ->
core.loadConfigFromXml(corePreferences.thirdPartyDefaultValuesPath)
val user = username.value.orEmpty().trim()
// Remove sip: in front of domain, just in case...
val domainValue = domain.value.orEmpty().trim()
val domain = if (domainValue.startsWith("sip:")) {
domainValue.substring("sip:".length)
} else {
domainValue
}
// Allow to enter SIP identity instead of simply username
// in case identity domain doesn't match proxy domain
val user = username.value.orEmpty().trim()
val identity = if (user.startsWith("sip:")) {
if (user.contains("@")) {
user
} else {
"$user@$domain"
}
} else {
if (user.contains("@")) {
"sip:$user"
} else {
"sip:$user@$domain"
}
}
val identityAddress = Factory.instance().createAddress(identity)
newlyCreatedAuthInfo = Factory.instance().createAuthInfo(
user,
@ -152,19 +172,18 @@ class ThirdPartySipAccountLoginViewModel @UiThread constructor() : GenericViewMo
password.value.orEmpty().trim(),
null,
null,
domainValue
null
)
core.addAuthInfo(newlyCreatedAuthInfo)
val accountParams = core.createAccountParams()
val identityAddress = Factory.instance().createAddress("sip:$user@$domainValue")
if (displayName.value.orEmpty().isNotEmpty()) {
identityAddress?.displayName = displayName.value.orEmpty().trim()
}
accountParams.identityAddress = identityAddress
val serverAddress = Factory.instance().createAddress("sip:$domainValue")
val serverAddress = Factory.instance().createAddress("sip:$domain")
serverAddress?.transport = when (transport.value.orEmpty().trim()) {
TransportType.Tcp.name.uppercase(Locale.getDefault()) -> TransportType.Tcp
TransportType.Tls.name.uppercase(Locale.getDefault()) -> TransportType.Tls
@ -204,6 +223,7 @@ class ThirdPartySipAccountLoginViewModel @UiThread constructor() : GenericViewMo
@UiThread
private fun isLoginButtonEnabled(): Boolean {
return username.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty() && domain.value.orEmpty().isNotEmpty()
// Password isn't mandatory as authentication could be Bearer
return username.value.orEmpty().isNotEmpty() && domain.value.orEmpty().isNotEmpty()
}
}

View file

@ -24,8 +24,8 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.navArgs
import androidx.navigation.navGraphViewModels
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationResponse
import org.linphone.R
@ -44,9 +44,7 @@ class SingleSignOnFragment : GenericMainFragment() {
private lateinit var binding: SingleSignOnFragmentBinding
private val viewModel: SingleSignOnViewModel by navGraphViewModels(
R.id.main_nav_graph
)
private lateinit var viewModel: SingleSignOnViewModel
private val args: SingleSignOnFragmentArgs by navArgs()
@ -62,6 +60,8 @@ class SingleSignOnFragment : GenericMainFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
viewModel = ViewModelProvider(this)[SingleSignOnViewModel::class.java]
binding.viewModel = viewModel
observeToastEvents(viewModel)

View file

@ -114,6 +114,7 @@ class SingleSignOnViewModel : GenericViewModel() {
"$singleSignOnUrl/.well-known/openid-configuration"
}
singleSignOn()
return@RetrieveConfigurationCallback
} else {
onErrorEvent.postValue(Event("Failed to fetch configuration"))
return@RetrieveConfigurationCallback
@ -158,34 +159,48 @@ class SingleSignOnViewModel : GenericViewModel() {
authService = AuthorizationService(coreContext.context)
}
val authStateJsonFile = File(
coreContext.context.filesDir.absolutePath,
"auth_state.json"
)
Log.i("$TAG Starting refresh token request")
authService.performTokenRequest(
authState.createTokenRefreshRequest()
) { resp, ex ->
if (resp != null) {
Log.i("$TAG Token refresh succeeded!")
try {
authService.performTokenRequest(
authState.createTokenRefreshRequest()
) { resp, ex ->
if (resp != null) {
Log.i("$TAG Token refresh succeeded!")
if (::authState.isInitialized) {
Log.i("$TAG Updating AuthState object after refresh token response")
authState.update(resp, ex)
storeAuthStateAsJsonFile()
}
updateTokenInfo()
} else {
Log.e(
"$TAG Failed to perform token refresh [$ex], destroying auth_state.json file"
)
onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty()))
val file = File(coreContext.context.filesDir.absolutePath, "auth_state.json")
viewModelScope.launch {
FileUtils.deleteFile(file.absolutePath)
Log.w(
"$TAG Previous auth_state.json file deleted, starting single sign on process from scratch"
if (::authState.isInitialized) {
Log.i("$TAG Updating AuthState object after refresh token response")
authState.update(resp, ex)
storeAuthStateAsJsonFile()
}
updateTokenInfo()
} else {
Log.e(
"$TAG Failed to perform token refresh [$ex], destroying auth_state.json file"
)
singleSignOn()
onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty()))
viewModelScope.launch {
FileUtils.deleteFile(authStateJsonFile.absolutePath)
Log.w(
"$TAG Previous auth_state.json file deleted, starting single sign on process from scratch"
)
singleSignOn()
}
}
}
} catch (ise: IllegalStateException) {
Log.e("$TAG Illegal state exception, clearing auth state and trying again: $ise")
viewModelScope.launch {
FileUtils.deleteFile(authStateJsonFile.absolutePath)
authState = getAuthState()
performRefreshToken()
}
} catch (e: Exception) {
Log.e("$TAG Failed to perform token request: $e")
}
}
}

View file

@ -70,7 +70,7 @@
android:layout_marginTop="38dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@{@string/username + `*`, default=`Username*`}"
android:text="@string/assistant_third_party_sip_account_username_or_identity"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="@id/username"/>

View file

@ -168,4 +168,27 @@
app:destination="@id/profileModeFragment" />
</fragment>
<fragment
android:id="@+id/singleSignOnFragment"
android:name="org.linphone.ui.main.sso.fragment.SingleSignOnFragment"
android:label="SingleSignOnFragment"
tools:layout="@layout/single_sign_on_fragment">
<argument
android:name="serverUrl"
app:argType="string" />
<argument
android:name="username"
app:argType="string"
app:nullable="true"
android:defaultValue="@null" />
</fragment>
<action
android:id="@+id/action_global_singleSignOnFragment"
app:destination="@id/singleSignOnFragment"
app:launchSingleTop="true"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out" />
</navigation>

View file

@ -137,6 +137,7 @@
<string name="assistant_third_party_sip_account_warning_explanation">Certaines fonctionalités telles que les conversations de groupe, les vidéo-conférences, etc… nécessitent un compte &appName;.\n\nCes fonctionalités seront masquées si vous utilisez un compte SIP tiers.\n\nPour les activer dans un projet commercial, merci de nous contacter.</string>
<string name="assistant_third_party_sip_account_create_linphone_account">Je préfère créer un compte</string>
<string name="assistant_third_party_sip_account_warning_ok">J\'ai compris</string>
<string name="assistant_third_party_sip_account_username_or_identity">Nom d\'utilisateur ou identité SIP*</string>
<string name="assistant_account_secure_mode_title">Choisissez votre mode</string>
<string name="assistant_account_secure_mode_subtitle">Vous pourrez le changer plus tard</string>
<string name="assistant_secure_mode_finish_account_login">Continuer</string>

View file

@ -172,6 +172,7 @@
<string name="assistant_third_party_sip_account_warning_explanation">Some features require a &appName; account, such as group messaging, video conferences…\n\nThese features are hidden when you register with a third party SIP account.\n\nTo enable it in a commercial project, please contact us.</string>
<string name="assistant_third_party_sip_account_create_linphone_account">I prefer to create an account</string>
<string name="assistant_third_party_sip_account_warning_ok">I understand</string>
<string name="assistant_third_party_sip_account_username_or_identity">Username or SIP identity*</string>
<string name="assistant_account_secure_mode_title">Personalize your profile mode</string>
<string name="assistant_account_secure_mode_subtitle">You may change that mode later</string>
<string name="assistant_secure_mode_finish_account_login">Continue</string>