Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support work account creation through device policy controller #2521

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
38 changes: 38 additions & 0 deletions play-services-auth-workaccount/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2023 e foundation
* SPDX-License-Identifier: Apache-2.0
*/

apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
apply plugin: 'signing'

android {
namespace "com.google.android.gms.auth.workaccount"

compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"

buildFeatures {
aidl = true
}

defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}

compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}

}

apply from: '../gradle/publish-android.gradle'

description = 'microG implementation of managed work account support'

dependencies {
}
45 changes: 45 additions & 0 deletions play-services-auth-workaccount/core/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: 2023 e foundation
* SPDX-License-Identifier: Apache-2.0
*/

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

dependencies {
api project(':play-services-auth-workaccount')
api project(':play-services-auth')
implementation project(':play-services-base-core')

implementation "androidx.appcompat:appcompat:$appcompatVersion"
}

android {
namespace "com.google.android.gms.auth.workaccount"

compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"

defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}

compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}

kotlinOptions {
jvmTarget = 1.8
}

lintOptions {
disable 'MissingTranslation'
}
}
49 changes: 49 additions & 0 deletions play-services-auth-workaccount/core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2023 e foundation
~ SPDX-License-Identifier: Apache-2.0
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission
android:name="android.permission.AUTHENTICATE_ACCOUNTS"
android:maxSdkVersion="22" />
<uses-permission
android:name="android.permission.GET_ACCOUNTS"
android:maxSdkVersion="22" />
<uses-permission
android:name="android.permission.MANAGE_ACCOUNTS"
android:maxSdkVersion="22" />

<application>

<service android:name="org.microg.gms.auth.workaccount.WorkAccountService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.auth.account.workaccount.START" />
</intent-filter>
</service>

<service
android:name="com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticatorService"
android:process=":persistent"
android:enabled="false"
android:exported="false">

<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>

<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/auth_work_authenticator"/>

<meta-data
android:name="android.accounts.AccountAuthenticator.customTokens"
android:value="1"/>

</service>

</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* SPDX-FileCopyrightText: 2024 e foundation
* SPDX-License-Identifier: Apache-2.0
*/

package com.google.android.gms.auth.account.authenticator

import android.accounts.AbstractAccountAuthenticator
import android.accounts.Account
import android.accounts.AccountAuthenticatorResponse
import android.accounts.AccountManager
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.util.Log
import com.google.android.gms.auth.workaccount.R
import org.microg.gms.auth.workaccount.AuthRequest

class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthenticator(context) {

override fun editProperties(
response: AccountAuthenticatorResponse,
accountType: String?
): Bundle {
TODO("Not yet implemented: editProperties")
}

override fun addAccount(
response: AccountAuthenticatorResponse,
accountType: String,
authTokenType: String?,
requiredFeatures: Array<out String>?,
options: Bundle
): Bundle? {
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)"
)

// 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)!!

try {
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, 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(KEY_ACCOUNT_CAPABILITIES, authResponse.capabilities)
putString(KEY_ACCOUNT_SERVICES, authResponse.services) // expected to be "android"
}
)) {
// Report successful creation to caller
response.onResult(Bundle().apply {
putString(AccountManager.KEY_ACCOUNT_NAME, authResponse.email)
putString(AccountManager.KEY_ACCOUNT_TYPE, WORK_ACCOUNT_TYPE)
})
}

} catch (exception: Exception) {
response.onResult(Bundle().apply {
putInt(
AccountManager.KEY_ERROR_CODE,
AccountManager.ERROR_CODE_NETWORK_ERROR
)
putString(AccountManager.KEY_ERROR_MESSAGE, exception.message)
})
}

/* 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
}

override fun confirmCredentials(
response: AccountAuthenticatorResponse?,
account: Account?,
options: Bundle?
): Bundle {
return Bundle().apply {
putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true)
}
}

override fun getAuthToken(
response: AccountAuthenticatorResponse?,
account: Account?,
authTokenType: String?,
options: Bundle?
): Bundle {
TODO("Not yet implemented: getAuthToken")
}

override fun getAuthTokenLabel(authTokenType: String?): String {
TODO("Not yet implemented: getAuthTokenLabel")
}

override fun updateCredentials(
response: AccountAuthenticatorResponse?,
account: Account?,
authTokenType: String?,
options: Bundle?
): Bundle {
TODO("Not yet implemented: updateCredentials")
}

override fun hasFeatures(
response: AccountAuthenticatorResponse?,
account: Account?,
features: Array<out String>
): Bundle {
Log.i(TAG, "Queried features: " + features.joinToString(", "))
return Bundle().apply {
putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false)
}
}

/**
* Prevent accidental deletion, unlike GMS. The account can only be removed through client apps;
* ideally, it would only be removed by the app that requested it to be created / the DPC
* manager, though this is not enforced. On API 21, the account can also be removed by hand
* because `removeAccountExplicitly` is not available on API 21.
*/
override fun getAccountRemovalAllowed(
response: AccountAuthenticatorResponse?,
account: Account?
): Bundle {
return Bundle().apply {
putBoolean(AccountManager.KEY_BOOLEAN_RESULT,
Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1
)
}
}

companion object {
const val TAG = "WorkAccAuthenticator"
const val WORK_ACCOUNT_TYPE = "com.google.work"
const val KEY_ACCOUNT_CREATION_TOKEN = "creationToken"
private const val KEY_GOOGLE_USER_ID = "GoogleUserId" // TODO: use AuthConstants
private const val KEY_ACCOUNT_SERVICES = "services" // TODO: use AuthConstants
private const val KEY_ACCOUNT_CAPABILITIES = "capabilities"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2024 e foundation
* SPDX-License-Identifier: Apache-2.0
*/

package com.google.android.gms.auth.account.authenticator

import android.accounts.AccountManager
import android.app.Service
import android.content.Intent
import android.os.IBinder

class WorkAccountAuthenticatorService : Service() {
private val authenticator by lazy { WorkAccountAuthenticator(this) }

override fun onBind(intent: Intent): IBinder? {
if (intent.action == AccountManager.ACTION_AUTHENTICATOR_INTENT) {
return authenticator.iBinder
}
return null
}
}
Loading