From 0edad33c89f483f86cd91bdc4080e825a5bacfe4 Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Tue, 3 Jun 2025 13:36:06 -0400 Subject: [PATCH] android: automatically trigger login flow for authKey Signed-off-by: Jonathan Nobels --- .../java/com/tailscale/ipn/MainActivity.kt | 9 ++-- .../java/com/tailscale/ipn/mdm/MDMSettings.kt | 6 ++- .../ipn/ui/viewModel/IpnViewModel.kt | 22 ++++----- .../ipn/ui/viewModel/MainViewModel.kt | 47 +++++++++++++++++++ 4 files changed, 68 insertions(+), 16 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/MainActivity.kt b/android/src/main/java/com/tailscale/ipn/MainActivity.kt index 8f72dcf..a2c81f0 100644 --- a/android/src/main/java/com/tailscale/ipn/MainActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/MainActivity.kt @@ -494,10 +494,6 @@ class MainActivity : ComponentActivity() { lifecycleScope.launch(Dispatchers.IO) { MDMSettings.update(App.get(), restrictionsManager) } } - override fun onStart() { - super.onStart() - } - override fun onStop() { super.onStop() val restrictionsManager = @@ -515,6 +511,11 @@ class MainActivity : ComponentActivity() { } private fun introScreenViewed(): Boolean { + // If the auth key is set, there is no need to render the intro screen. + if (MDMSettings.authKey.flow.value.value != null || + MDMSettings.onboardingFlow.flow.value.value) { + return true + } return getSharedPreferences("introScreen", Context.MODE_PRIVATE).getBoolean("seen", false) } diff --git a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt index c843d90..f45078f 100644 --- a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt +++ b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt @@ -20,6 +20,8 @@ object MDMSettings { val forceEnabled = BooleanMDMSetting("ForceEnabled", "Force Enabled Connection Toggle") + val onboardingFlow = BooleanMDMSetting("OnboardingFlow", "Skip Onboarding Flow") + // Handled on the backed val exitNodeID = StringMDMSetting("ExitNodeID", "Forced Exit Node: Stable ID") @@ -61,7 +63,8 @@ object MDMSettings { // Handled on the backend val deviceSerialNumber = - StringMDMSetting("DeviceSerialNumber", "Serial number of the device that is running Tailscale") + StringMDMSetting( + "DeviceSerialNumber", "Serial number of the device that is running Tailscale") val useTailscaleDNSSettings = AlwaysNeverUserDecidesMDMSetting("UseTailscaleDNSSettings", "Use Tailscale DNS Settings") @@ -97,6 +100,7 @@ object MDMSettings { // Handled on the backend val authKey = StringMDMSetting("AuthKey", "Auth Key for login") + // tskey-auth-kBUy7NiRQ411CNTRL-a8sEkPG4KcDP2Kts1CTRdDfjkDSxat5T // Overrides the value provided by os.Hostname() in Go val hostname = StringMDMSetting("Hostname", "Device Hostname") diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt index 08b0da6..56d74af 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/IpnViewModel.kt @@ -182,18 +182,18 @@ open class IpnViewModel : ViewModel() { completionHandler(Result.failure(it)) } .onSuccess { - if (authKey.isNullOrEmpty()) { - client.startLoginInteractive { loginResult -> - loginResult - .onFailure { - TSLog.e(TAG, "startLoginInteractive() failed: ${it.message}") - completionHandler(Result.failure(it)) - } - .onSuccess { completionHandler(Result.success(Unit)) } - } - } else { - completionHandler(Result.success(Unit)) + // if (authKey.isNullOrEmpty()) { + client.startLoginInteractive { loginResult -> + loginResult + .onFailure { + TSLog.e(TAG, "startLoginInteractive() failed: ${it.message}") + completionHandler(Result.failure(it)) + } + .onSuccess { completionHandler(Result.success(Unit)) } } + // } else { + // completionHandler(Result.success(Unit)) + // } } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt index 332c77e..e63d496 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt @@ -50,6 +50,14 @@ class MainViewModelFactory(private val vpnViewModel: VpnViewModel) : ViewModelPr @OptIn(FlowPreview::class) class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { + + companion object AuthKeyTracking { + data class AuthKeyFailure(val authKey: String? = null, var attempts: Int = 0) + + // Non-null if we have tried to login with an auth key and it failed. + var authKeyFailure: AuthKeyFailure? = null + } + // The user readable state of the system val stateRes: StateFlow = MutableStateFlow(userStringRes(State.NoState, State.NoState, true)) @@ -149,6 +157,40 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { // Update the VPN toggle state _vpnToggleState.value = isOn + val authKey = getMDMAuthKey() + if (currentState == State.NeedsLogin && + previousState != State.NeedsLogin && + authKey != null) { + + // Same auth key? Too many attempts? Stop trying, at least for the duration + // of this app session. + if (authKeyFailure != null && + authKeyFailure!!.authKey != authKey && + authKeyFailure!!.attempts > 3) { + TSLog.e("MainViewModel", "Failed to login with auth key: too many failed attempts") + } else { + // If we have an MDM'd auth key, try to login automatically + TSLog.d("MainViewModel", "Logging in with MDM auth key") + loginWithAuthKey(authKey) { + it.onFailure { error -> + TSLog.e("MainViewModel", "Failed to auto login with auth key: $error") + var failureState = authKeyFailure ?: AuthKeyFailure(authKey, 0) + if (authKey == failureState.authKey) { + failureState.attempts = failureState.attempts + 1 + } else { + // New auth key... Reset our attempts + failureState.attempts = 0 + } + authKeyFailure = failureState + } + it.onSuccess { + authKeyFailure = null + // Successfully logged in, no further action needed + } + } + } + } + // Update the previous state previousState = currentState } @@ -197,6 +239,11 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() { } } + fun getMDMAuthKey(): String { + return "tskey-auth-kBUy7NiRQ411CNTRL-a8sEkPG4KcDP2Kts1CTRdDfjkDSxat5T" + /// return MDMSettings.authKey.flow.value.value + } + fun maybeRequestVpnPermission() { _requestVpnPermission.value = true }