1+ /*
2+ * SPDX-FileCopyrightText: 2024 e foundation
3+ * SPDX-License-Identifier: Apache-2.0
4+ */
5+
6+ package com.google.android.gms.auth.account.authenticator
7+
8+ import android.accounts.AbstractAccountAuthenticator
9+ import android.accounts.Account
10+ import android.accounts.AccountAuthenticatorResponse
11+ import android.accounts.AccountManager
12+ import android.content.Context
13+ import android.content.Intent
14+ import android.os.Build
15+ import android.os.Bundle
16+ import android.util.Log
17+ import com.google.android.gms.auth.workaccount.R
18+ import org.microg.gms.auth.AuthConstants
19+ import org.microg.gms.common.PackageUtils
20+ import org.microg.gms.auth.AuthRequest
21+ import org.microg.gms.auth.AuthResponse
22+ import org.microg.gms.auth.workaccount.WorkProfileSettings
23+ import java.io.IOException
24+ import kotlin.jvm.Throws
25+
26+ class WorkAccountAuthenticator (val context : Context ) : AbstractAccountAuthenticator(context) {
27+
28+ override fun editProperties (
29+ response : AccountAuthenticatorResponse ,
30+ accountType : String?
31+ ): Bundle {
32+ TODO (" Not yet implemented: editProperties" )
33+ }
34+
35+ override fun addAccount (
36+ response : AccountAuthenticatorResponse ,
37+ accountType : String ,
38+ authTokenType : String? ,
39+ requiredFeatures : Array <out String >? ,
40+ options : Bundle
41+ ): Bundle ? {
42+
43+ if (! WorkProfileSettings (context).allowCreateWorkAccount) {
44+ return Bundle ().apply {
45+ putInt(AccountManager .KEY_ERROR_CODE , AccountManager .ERROR_CODE_UNSUPPORTED_OPERATION )
46+ putString(AccountManager .KEY_ERROR_MESSAGE , context.getString(R .string.auth_work_authenticator_disabled_error)
47+ )
48+ }
49+ } else if (
50+ ! options.containsKey(KEY_ACCOUNT_CREATION_TOKEN )
51+ || options.getString(KEY_ACCOUNT_CREATION_TOKEN ) == null
52+ || options.getInt(AccountManager .KEY_CALLER_UID ) != android.os.Process .myUid()) {
53+ Log .e(TAG ,
54+ " refusing to add account without creation token or from external app: " +
55+ " could have been manually initiated by user (not supported) " +
56+ " or by unauthorized app (not allowed)"
57+ )
58+
59+ // TODO: The error message is not automatically displayed by the settings app as of now.
60+ // We can consider showing the error message through a popup instead.
61+
62+ return Bundle ().apply {
63+ putInt(AccountManager .KEY_ERROR_CODE , AccountManager .ERROR_CODE_UNSUPPORTED_OPERATION )
64+ putString(AccountManager .KEY_ERROR_MESSAGE , context.getString(R .string.auth_work_authenticator_add_manual_error)
65+ )
66+ }
67+ }
68+
69+ val oauthToken: String = options.getString(KEY_ACCOUNT_CREATION_TOKEN )!!
70+
71+ try {
72+ tryAddAccount(oauthToken, response)
73+ } catch (exception: Exception ) {
74+ response.onResult(Bundle ().apply {
75+ putInt(
76+ AccountManager .KEY_ERROR_CODE ,
77+ AccountManager .ERROR_CODE_NETWORK_ERROR
78+ )
79+ putString(AccountManager .KEY_ERROR_MESSAGE , exception.message)
80+ })
81+ }
82+
83+ /* Note: as is not documented, `null` must only be returned after `response.onResult` was
84+ * already called, hence forcing the requests to be synchronous. They are still async to
85+ * the caller's main thread because AccountManager forces potentially blocking operations,
86+ * like waiting for a response upon `addAccount`, not to be on the main thread.
87+ */
88+ return null
89+ }
90+
91+ @Throws(Exception ::class )
92+ private fun tryAddAccount (
93+ oauthToken : String ,
94+ response : AccountAuthenticatorResponse
95+ ) {
96+ val authResponse = AuthRequest ().fromContext(context)
97+ .appIsGms()
98+ .callerIsGms()
99+ .service(" ac2dm" )
100+ .token(oauthToken).isAccessToken()
101+ .addAccount()
102+ .getAccountId()
103+ .droidguardResults(null )
104+ .response
105+
106+ val accountManager = AccountManager .get(context)
107+ if (accountManager.addAccountExplicitly(
108+ Account (authResponse.email, AuthConstants .WORK_ACCOUNT_TYPE ),
109+ authResponse.token, Bundle ().apply {
110+ // Work accounts have no SID / LSID ("BAD_COOKIE") and no first/last name.
111+ if (authResponse.accountId.isNotBlank()) {
112+ putString(KEY_GOOGLE_USER_ID , authResponse.accountId)
113+ }
114+ putString(AuthConstants .KEY_ACCOUNT_CAPABILITIES , authResponse.capabilities)
115+ putString(AuthConstants .KEY_ACCOUNT_SERVICES , authResponse.services)
116+ if (authResponse.services != " android" ) {
117+ Log .i(
118+ TAG ,
119+ " unexpected 'services' value ${authResponse.services} (usually 'android')"
120+ )
121+ }
122+ }
123+ )
124+ ) {
125+
126+ // Notify vending package
127+ context.sendBroadcast(
128+ Intent (WORK_ACCOUNT_CHANGED_BOARDCAST ).setPackage(" com.android.vending" )
129+ )
130+
131+ // Report successful creation to caller
132+ response.onResult(Bundle ().apply {
133+ putString(AccountManager .KEY_ACCOUNT_NAME , authResponse.email)
134+ putString(AccountManager .KEY_ACCOUNT_TYPE , AuthConstants .WORK_ACCOUNT_TYPE )
135+ })
136+ }
137+ }
138+
139+ override fun confirmCredentials (
140+ response : AccountAuthenticatorResponse ? ,
141+ account : Account ? ,
142+ options : Bundle ?
143+ ): Bundle {
144+ return Bundle ().apply {
145+ putBoolean(AccountManager .KEY_BOOLEAN_RESULT , true )
146+ }
147+ }
148+
149+ override fun getAuthToken (
150+ response : AccountAuthenticatorResponse ? ,
151+ account : Account ,
152+ authTokenType : String? ,
153+ options : Bundle ?
154+ ): Bundle {
155+ try {
156+ val authResponse: AuthResponse =
157+ AuthRequest ().fromContext(context)
158+ .source(" android" )
159+ .app(
160+ context.packageName,
161+ PackageUtils .firstSignatureDigest(context, context.packageName)
162+ )
163+ .email(account.name)
164+ .token(AccountManager .get(context).getPassword(account))
165+ .service(authTokenType)
166+ .delegation(0 , null )
167+ // .oauth2Foreground(oauth2Foreground)
168+ // .oauth2Prompt(oauth2Prompt)
169+ // .oauth2IncludeProfile(includeProfile)
170+ // .oauth2IncludeEmail(includeEmail)
171+ // .itCaveatTypes(itCaveatTypes)
172+ // .tokenRequestOptions(tokenRequestOptions)
173+ .systemPartition(true )
174+ .hasPermission(true )
175+ // .putDynamicFiledMap(dynamicFields)
176+ .appIsGms()
177+ .callerIsApp()
178+ .response
179+
180+ return Bundle ().apply {
181+ putString(AccountManager .KEY_ACCOUNT_NAME , account.name)
182+ putString(AccountManager .KEY_ACCOUNT_TYPE , account.type)
183+ putString(AccountManager .KEY_AUTHTOKEN , authResponse.auth)
184+ }
185+ } catch (e: IOException ) {
186+ return Bundle ().apply {
187+ putInt(AccountManager .KEY_ERROR_CODE , AccountManager .ERROR_CODE_NETWORK_ERROR )
188+ putString(AccountManager .KEY_ERROR_MESSAGE , e.message)
189+ }
190+ }
191+ }
192+
193+ override fun getAuthTokenLabel (authTokenType : String? ): String {
194+ TODO (" Not yet implemented: getAuthTokenLabel" )
195+ }
196+
197+ override fun updateCredentials (
198+ response : AccountAuthenticatorResponse ? ,
199+ account : Account ? ,
200+ authTokenType : String? ,
201+ options : Bundle ?
202+ ): Bundle {
203+ TODO (" Not yet implemented: updateCredentials" )
204+ }
205+
206+ override fun hasFeatures (
207+ response : AccountAuthenticatorResponse ? ,
208+ account : Account ? ,
209+ features : Array <out String >
210+ ): Bundle {
211+ Log .i(TAG , " Queried features: " + features.joinToString(" , " ))
212+ return Bundle ().apply {
213+ putBoolean(AccountManager .KEY_BOOLEAN_RESULT , false )
214+ }
215+ }
216+
217+ /* *
218+ * Prevent accidental deletion, unlike GMS. The account can only be removed through client apps;
219+ * ideally, it would only be removed by the app that requested it to be created / the DPC
220+ * manager, though this is not enforced. On API 21, the account can also be removed by hand
221+ * because `removeAccountExplicitly` is not available on API 21.
222+ */
223+ override fun getAccountRemovalAllowed (
224+ response : AccountAuthenticatorResponse ? ,
225+ account : Account ?
226+ ): Bundle {
227+ return Bundle ().apply {
228+ putBoolean(AccountManager .KEY_BOOLEAN_RESULT ,
229+ Build .VERSION .SDK_INT < Build .VERSION_CODES .LOLLIPOP_MR1
230+ )
231+ }
232+ }
233+
234+ companion object {
235+ const val TAG = " WorkAccAuthenticator"
236+
237+ const val WORK_ACCOUNT_CHANGED_BOARDCAST = " org.microg.vending.WORK_ACCOUNT_CHANGED"
238+
239+ const val KEY_ACCOUNT_CREATION_TOKEN = " creationToken"
240+ private const val KEY_GOOGLE_USER_ID = AuthConstants .GOOGLE_USER_ID
241+ }
242+ }
0 commit comments