From 3020a99aeb63c918bb7e2c8d8174f7920af45188 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Fri, 20 Mar 2026 19:30:26 +0100 Subject: [PATCH 1/3] workaccount: Eliminate call to `AccountManager.addAccount` Intune now sets a policy such that users are not allowed to create work profiles anymore. This is good in principle because users are never supposed to do this manually (and are, in fact, incapable of doing so). Our code is also affected because `WorkAccountService` triggered sign-in flow in `WorkAccountAuthenticator` through `AccountManager.addAccount`, which is the action prohibited by the policy. We can still (and need to) call `AccountManager.addAccountExplicitly` to register the account with the system. --- .../authenticator/WorkAccountAuthenticator.kt | 140 +++++++----------- .../auth/workaccount/WorkAccountService.kt | 30 +--- 2 files changed, 59 insertions(+), 111 deletions(-) diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt index 62e31a3f61..c1d0289be5 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024 e foundation + * SPDX-FileCopyrightText: 2026 e foundation * SPDX-License-Identifier: Apache-2.0 */ @@ -40,99 +40,68 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica options: Bundle ): Bundle? { - if (!WorkProfileSettings(context).allowCreateWorkAccount) { - return Bundle().apply { - putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION) - putString(AccountManager.KEY_ERROR_MESSAGE, context.getString(R.string.auth_work_authenticator_disabled_error) - ) - } - } else if ( - !options.containsKey(KEY_ACCOUNT_CREATION_TOKEN) - || options.getString(KEY_ACCOUNT_CREATION_TOKEN) == null - || options.getInt(AccountManager.KEY_CALLER_UID) != android.os.Process.myUid()) { - Log.e(TAG, - "refusing to add account without creation token or from external app: " + - "could have been manually initiated by user (not supported) " + - "or by unauthorized app (not allowed)" + return Bundle().apply { + putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION) + putString( + AccountManager.KEY_ERROR_MESSAGE, + context.getString(R.string.auth_work_authenticator_add_manual_error) ) - - // TODO: The error message is not automatically displayed by the settings app as of now. - // We can consider showing the error message through a popup instead. - - return Bundle().apply { - putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION) - putString(AccountManager.KEY_ERROR_MESSAGE, context.getString(R.string.auth_work_authenticator_add_manual_error) - ) - } } + } - val oauthToken: String = options.getString(KEY_ACCOUNT_CREATION_TOKEN)!! + fun addAccountInternal( + accountCreationToken: String + ): Account? { - try { - tryAddAccount(oauthToken, response) - } catch (exception: Exception) { - response.onResult(Bundle().apply { - putInt( - AccountManager.KEY_ERROR_CODE, - AccountManager.ERROR_CODE_NETWORK_ERROR - ) - putString(AccountManager.KEY_ERROR_MESSAGE, exception.message) - }) + if (!WorkProfileSettings(context).allowCreateWorkAccount) { + // TODO: communicate error to user (use `R.string.auth_work_authenticator_disabled_error)`) + Log.w(TAG, "creating a work account is disabled in microG settings") + return null } - /* Note: as is not documented, `null` must only be returned after `response.onResult` was - * already called, hence forcing the requests to be synchronous. They are still async to - * the caller's main thread because AccountManager forces potentially blocking operations, - * like waiting for a response upon `addAccount`, not to be on the main thread. - */ - return null - } - - @Throws(Exception::class) - private fun tryAddAccount( - oauthToken: String, - response: AccountAuthenticatorResponse - ) { - val authResponse = AuthRequest().fromContext(context) - .appIsGms() - .callerIsGms() - .service("ac2dm") - .token(oauthToken).isAccessToken() - .addAccount() - .getAccountId() - .droidguardResults(null) - .response - - val accountManager = AccountManager.get(context) - if (accountManager.addAccountExplicitly( - Account(authResponse.email, AuthConstants.WORK_ACCOUNT_TYPE), - authResponse.token, Bundle().apply { - // Work accounts have no SID / LSID ("BAD_COOKIE") and no first/last name. - if (authResponse.accountId.isNotBlank()) { - putString(KEY_GOOGLE_USER_ID, authResponse.accountId) - } - putString(AuthConstants.KEY_ACCOUNT_CAPABILITIES, authResponse.capabilities) - putString(AuthConstants.KEY_ACCOUNT_SERVICES, authResponse.services) - if (authResponse.services != "android") { - Log.i( - TAG, - "unexpected 'services' value ${authResponse.services} (usually 'android')" - ) + try { + val authResponse = AuthRequest().fromContext(context) + .appIsGms() + .callerIsGms() + .service("ac2dm") + .token(accountCreationToken).isAccessToken() + .addAccount() + .getAccountId() + .droidguardResults(null) + .response + + val accountManager = AccountManager.get(context) + val account = Account(authResponse.email, AuthConstants.WORK_ACCOUNT_TYPE) + if (accountManager.addAccountExplicitly( + account, + authResponse.token, Bundle().apply { + // Work accounts have no SID / LSID ("BAD_COOKIE") and no first/last name. + if (authResponse.accountId.isNotBlank()) { + putString(KEY_GOOGLE_USER_ID, authResponse.accountId) + } + putString(AuthConstants.KEY_ACCOUNT_CAPABILITIES, authResponse.capabilities) + putString(AuthConstants.KEY_ACCOUNT_SERVICES, authResponse.services) + if (authResponse.services != "android") { + Log.i( + TAG, + "unexpected 'services' value ${authResponse.services} (usually 'android')" + ) + } } - } - ) - ) { + ) + ) { - // Notify vending package - context.sendBroadcast( - Intent(WORK_ACCOUNT_CHANGED_BOARDCAST).setPackage("com.android.vending") - ) + // Notify vending package + context.sendBroadcast( + Intent(WORK_ACCOUNT_CHANGED_BOARDCAST).setPackage("com.android.vending") + ) - // Report successful creation to caller - response.onResult(Bundle().apply { - putString(AccountManager.KEY_ACCOUNT_NAME, authResponse.email) - putString(AccountManager.KEY_ACCOUNT_TYPE, AuthConstants.WORK_ACCOUNT_TYPE) - }) + // Report successful creation to caller + return account + } else return null + } catch (exception: Exception) { + Log.w(TAG, exception) + return null } } @@ -234,7 +203,6 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica const val WORK_ACCOUNT_CHANGED_BOARDCAST = "org.microg.vending.WORK_ACCOUNT_CHANGED" - const val KEY_ACCOUNT_CREATION_TOKEN = "creationToken" private const val KEY_GOOGLE_USER_ID = AuthConstants.GOOGLE_USER_ID } } \ No newline at end of file diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt index 21c3b77d21..bb8ff2b012 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2024 e foundation + * SPDX-FileCopyrightText: 2026 e foundation * SPDX-License-Identifier: Apache-2.0 */ @@ -13,12 +13,11 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.os.Build.VERSION.SDK_INT -import android.os.Bundle import android.os.Parcel import android.util.Log import com.google.android.gms.auth.account.IWorkAccountCallback import com.google.android.gms.auth.account.IWorkAccountService -import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticator.Companion.KEY_ACCOUNT_CREATION_TOKEN +import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticator import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticator.Companion.WORK_ACCOUNT_CHANGED_BOARDCAST import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticatorService import com.google.android.gms.common.Feature @@ -27,7 +26,6 @@ import com.google.android.gms.common.internal.ConnectionInfo import com.google.android.gms.common.internal.GetServiceRequest import com.google.android.gms.common.internal.IGmsCallbacks import org.microg.gms.BaseService -import org.microg.gms.auth.AuthConstants import org.microg.gms.common.GmsService import org.microg.gms.common.PackageUtils @@ -97,30 +95,12 @@ class WorkAccountServiceImpl(val context: Context) : IWorkAccountService.Stub() override fun addWorkAccount( callback: IWorkAccountCallback?, - token: String? + token: String ) { Log.d(TAG, "addWorkAccount with token $token") - val future = accountManager.addAccount( - AuthConstants.WORK_ACCOUNT_TYPE, - null, - null, - Bundle().apply { putString(KEY_ACCOUNT_CREATION_TOKEN, token) }, - null, - null, - null - ) Thread { - try { - future.result.let { result -> - callback?.onAccountAdded( - Account( - result.getString(AccountManager.KEY_ACCOUNT_NAME)!!, - result.getString(AccountManager.KEY_ACCOUNT_TYPE)!! - ) - ) - } - } catch (e: Exception) { - Log.e(TAG, "could not add work account with error message: ${e.message}") + WorkAccountAuthenticator(context).addAccountInternal(token)?.let { + callback?.onAccountAdded(it) } }.start() } From 2538cf62e67ebe57a501f329204d63a49301d9c3 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Fri, 20 Mar 2026 19:33:23 +0100 Subject: [PATCH 2/3] workaccount: fix sign in like 3a68bc3f1fd412cd6a9e3534d809b1ad01983423 i.e. same fix as https://github.com/microg/GmsCore/pull/3257/ --- .../gms/auth/account/authenticator/WorkAccountAuthenticator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt index c1d0289be5..3fe53967e6 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt @@ -67,7 +67,7 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica .token(accountCreationToken).isAccessToken() .addAccount() .getAccountId() - .droidguardResults(null) + .droidguardResults("null") // TODO .response val accountManager = AccountManager.get(context) From 50100bcd6fa4b9f7902dc9805c220423ac85ea74 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 22 Mar 2026 22:04:04 +0100 Subject: [PATCH 3/3] workaccount: cleanup --- .../authenticator/WorkAccountAuthenticator.kt | 62 +++++++++++-------- .../auth/workaccount/WorkAccountService.kt | 2 +- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt index 3fe53967e6..b5f3ff97e9 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2026 e foundation + * SPDX-FileCopyrightText: 2024 e foundation * SPDX-License-Identifier: Apache-2.0 */ @@ -38,8 +38,15 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica authTokenType: String?, requiredFeatures: Array?, options: Bundle - ): Bundle? { - + ): Bundle { + /* Calls to this method are always initiated by other applications or by the user. + * We refuse, because `accountCreationToken` is needed, and because only profile owner is + * supposed to provision this account. Profile owner will use `WorkAccountAuthenticator` + * instead, which calls the code in `addAccountInternal` directly. + * + * Also note: adding account with `AccountManager.addAccount` can be forbidden by device + * policy. + */ return Bundle().apply { putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION) putString( @@ -49,17 +56,20 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica } } + /** + * @return `null` if account creation fails, the newly created account otherwise + */ fun addAccountInternal( accountCreationToken: String ): Account? { if (!WorkProfileSettings(context).allowCreateWorkAccount) { - // TODO: communicate error to user (use `R.string.auth_work_authenticator_disabled_error)`) + // TODO: communicate error to user (use `R.string.auth_work_authenticator_disabled_error`) Log.w(TAG, "creating a work account is disabled in microG settings") return null } - try { + return try { val authResponse = AuthRequest().fromContext(context) .appIsGms() .callerIsGms() @@ -72,24 +82,24 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica val accountManager = AccountManager.get(context) val account = Account(authResponse.email, AuthConstants.WORK_ACCOUNT_TYPE) - if (accountManager.addAccountExplicitly( - account, - authResponse.token, Bundle().apply { - // Work accounts have no SID / LSID ("BAD_COOKIE") and no first/last name. - if (authResponse.accountId.isNotBlank()) { - putString(KEY_GOOGLE_USER_ID, authResponse.accountId) - } - putString(AuthConstants.KEY_ACCOUNT_CAPABILITIES, authResponse.capabilities) - putString(AuthConstants.KEY_ACCOUNT_SERVICES, authResponse.services) - if (authResponse.services != "android") { - Log.i( - TAG, - "unexpected 'services' value ${authResponse.services} (usually 'android')" - ) - } + val accountAdded = accountManager.addAccountExplicitly( + account, + authResponse.token, Bundle().apply { + // Work accounts have no SID / LSID ("BAD_COOKIE") and no first/last name. + if (authResponse.accountId.isNotBlank()) { + putString(KEY_GOOGLE_USER_ID, authResponse.accountId) } - ) - ) { + putString(AuthConstants.KEY_ACCOUNT_CAPABILITIES, authResponse.capabilities) + putString(AuthConstants.KEY_ACCOUNT_SERVICES, authResponse.services) + if (authResponse.services != "android") { + Log.i( + TAG, + "unexpected 'services' value ${authResponse.services} (usually 'android')" + ) + } + }) + + if (accountAdded) { // Notify vending package context.sendBroadcast( @@ -97,11 +107,11 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica ) // Report successful creation to caller - return account - } else return null + account + } else null } catch (exception: Exception) { - Log.w(TAG, exception) - return null + Log.w(TAG, "Failed to add work account.", exception) + null } } diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt index bb8ff2b012..4aa09a1542 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2026 e foundation + * SPDX-FileCopyrightText: 2024 e foundation * SPDX-License-Identifier: Apache-2.0 */