Commit d9dba28d authored by narinder Rana's avatar narinder Rana
Browse files

Merge branch '516-impl-google-account-setup' into '55-Update_accountManager_with_upstream'

impl goggle account setup

See merge request !52
parents 5ff8ec5a aef7d48c
......@@ -26,6 +26,10 @@ android {
buildConfigField "String", "userAgent", "\"DAVx5\""
manifestPlaceholders = [
'appAuthRedirectScheme': 'com.googleusercontent.apps.628867657910-7ade6gut5rhabdgjq6k4rln9i1u9ppca'
]
testInstrumentationRunner "at.bitfire.davdroid.CustomTestRunner"
kapt {
......@@ -151,7 +155,8 @@ dependencies {
implementation "org.apache.commons:commons-lang3:${versions.commonsLang}"
//noinspection GradleDependency
implementation "org.apache.commons:commons-text:${versions.commonsText}"
//google auth
implementation 'net.openid:appauth:0.7.0'
// for tests
androidTestImplementation "com.google.dagger:hilt-android-testing:${versions.hilt}"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:${versions.hilt}"
......
......@@ -280,6 +280,71 @@
android:resource="@xml/debug_paths" />
</provider>
<!-- account type "Google" -->
<service
android:name=".syncadapter.GoogleAccountAuthenticatorService"
android:exported="false">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/google_account_authenticator" />
</service>
<!-- account type "eelo" -->
<service
android:name=".syncadapter.EeloAccountAuthenticatorService"
android:exported="false">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/eelo_account_authenticator" />
</service>
<!-- Callback from authentication screen -->
<activity android:name="net.openid.appauth.RedirectUriReceiverActivity">
<!--
filter which captures custom scheme based redirects, which are used by the intermediary page when
the browser used for authentication cannot directly invoke this app. The source for the redirect
page can be found in web/oauth2redirect.html
-->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="net.openid.appauth.demo" />
<data android:scheme="com.googleusercontent.apps.100496780587-pbiu5eudcjm6cge2phduc6mt8mgbsmsr" />
</intent-filter>
<!--
for up-to-date Chrome browsers we can intercept the OAuth2 callback directly, if a secure assertion
is found on the site matching this app:
https://appauth.demo-app.io/.well-known/assetlinks.json
The source for this file can also be found in
web/.well-known/assetlinks.json
-->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="appauth.demo-app.io"
android:path="/oauth2redirect"
android:scheme="https" />
</intent-filter>
</activity>
</application>
<!-- package visiblity – which apps do we need to see? -->
......
package at.bitfire.davdroid.authorization;
/*
* Copyright 2015 The AppAuth Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.content.Context;
import android.content.res.Resources;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import foundation.e.accountmanager.R;
import net.openid.appauth.AuthorizationServiceConfiguration;
import net.openid.appauth.AuthorizationServiceConfiguration.RetrieveConfigurationCallback;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* An abstraction of identity providers, containing all necessary info for the demo app.
*/
public class IdentityProvider
{
/**
* Value used to indicate that a configured property is not specified or required.
*/
public static final int NOT_SPECIFIED = -1;
public static final IdentityProvider GOOGLE = new IdentityProvider(
"Google",
R.string.google_discovery_uri,
NOT_SPECIFIED, // auth endpoint is discovered
NOT_SPECIFIED, // token endpoint is discovered
R.string.google_client_id,
NOT_SPECIFIED, // client secret is not required for Google
R.string.google_auth_redirect_uri,
R.string.google_scope_string,
R.string.google_name);
public static final List<IdentityProvider> PROVIDERS = Arrays.asList(
GOOGLE);
public static List<IdentityProvider> getEnabledProviders(Context context)
{
ArrayList<IdentityProvider> providers = new ArrayList<>();
for (IdentityProvider provider : PROVIDERS)
{
provider.readConfiguration(context);
providers.add(provider);
}
return providers;
}
@NonNull
public final String name;
@StringRes
public final int buttonContentDescriptionRes;
@StringRes
private final int mDiscoveryEndpointRes;
@StringRes
private final int mAuthEndpointRes;
@StringRes
private final int mTokenEndpointRes;
@StringRes
private final int mClientIdRes;
@StringRes
private final int mClientSecretRes;
@StringRes
private final int mRedirectUriRes;
@StringRes
private final int mScopeRes;
private boolean mConfigurationRead = false;
private Uri mDiscoveryEndpoint;
private Uri mAuthEndpoint;
private Uri mTokenEndpoint;
private String mClientId;
private String mClientSecret;
private Uri mRedirectUri;
private String mScope;
IdentityProvider(
@NonNull String name,
@StringRes int discoveryEndpointRes,
@StringRes int authEndpointRes,
@StringRes int tokenEndpointRes,
@StringRes int clientIdRes,
@StringRes int clientSecretRes,
@StringRes int redirectUriRes,
@StringRes int scopeRes,
@StringRes int buttonContentDescriptionRes)
{
if (!isSpecified(discoveryEndpointRes)
&& !isSpecified(authEndpointRes)
&& !isSpecified(tokenEndpointRes))
{
throw new IllegalArgumentException(
"the discovery endpoint or the auth and token endpoints must be specified");
}
this.name = name;
this.mDiscoveryEndpointRes = discoveryEndpointRes;
this.mAuthEndpointRes = authEndpointRes;
this.mTokenEndpointRes = tokenEndpointRes;
this.mClientIdRes = checkSpecified(clientIdRes, "clientIdRes");
this.mClientSecretRes = clientSecretRes;
this.mRedirectUriRes = checkSpecified(redirectUriRes, "redirectUriRes");
this.mScopeRes = checkSpecified(scopeRes, "scopeRes");
this.buttonContentDescriptionRes =
checkSpecified(buttonContentDescriptionRes, "buttonContentDescriptionRes");
}
/**
* This must be called before any of the getters will function.
*/
public void readConfiguration(Context context)
{
if (mConfigurationRead)
{
return;
}
Resources res = context.getResources();
mDiscoveryEndpoint = isSpecified(mDiscoveryEndpointRes)
? getUriResource(res, mDiscoveryEndpointRes, "discoveryEndpointRes")
: null;
mAuthEndpoint = isSpecified(mAuthEndpointRes)
? getUriResource(res, mAuthEndpointRes, "authEndpointRes")
: null;
mTokenEndpoint = isSpecified(mTokenEndpointRes)
? getUriResource(res, mTokenEndpointRes, "tokenEndpointRes")
: null;
mClientId = res.getString(mClientIdRes);
mClientSecret = isSpecified(mClientSecretRes) ? res.getString(mClientSecretRes) : null;
mRedirectUri = getUriResource(res, mRedirectUriRes, "mRedirectUriRes");
mScope = res.getString(mScopeRes);
mConfigurationRead = true;
}
private void checkConfigurationRead()
{
if (!mConfigurationRead)
{
throw new IllegalStateException("Configuration not read");
}
}
@Nullable
public Uri getDiscoveryEndpoint()
{
checkConfigurationRead();
return mDiscoveryEndpoint;
}
@Nullable
public Uri getAuthEndpoint()
{
checkConfigurationRead();
return mAuthEndpoint;
}
@Nullable
public Uri getTokenEndpoint()
{
checkConfigurationRead();
return mTokenEndpoint;
}
@NonNull
public String getClientId()
{
checkConfigurationRead();
return mClientId;
}
@Nullable
public String getClientSecret()
{
checkConfigurationRead();
return mClientSecret;
}
@NonNull
public Uri getRedirectUri()
{
checkConfigurationRead();
return mRedirectUri;
}
@NonNull
public String getScope()
{
checkConfigurationRead();
return mScope;
}
public void retrieveConfig(Context context,
RetrieveConfigurationCallback callback)
{
readConfiguration(context);
if (getDiscoveryEndpoint() != null)
{
AuthorizationServiceConfiguration.fetchFromUrl(mDiscoveryEndpoint, callback);
}
else
{
AuthorizationServiceConfiguration config =
new AuthorizationServiceConfiguration(mAuthEndpoint, mTokenEndpoint, null);
callback.onFetchConfigurationCompleted(config, null);
}
}
private static boolean isSpecified(int value)
{
return value != NOT_SPECIFIED;
}
private static int checkSpecified(int value, String valueName)
{
if (value == NOT_SPECIFIED)
{
throw new IllegalArgumentException(valueName + " must be specified");
}
return value;
}
private static Uri getUriResource(Resources res, @StringRes int resId, String resName)
{
return Uri.parse(res.getString(resId));
}
}
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.db
package foundation.e.accountmanager.model
data class Credentials(
val userName: String? = null,
val password: String? = null,
val certificateAlias: String? = null
class Credentials(
val userName: String? = null,
val password: String? = null,
val accessToken: String? = null,
val refreshToken: String? = null,
val certificateAlias: String? = null
) {
override fun toString(): String {
val maskedPassword = "*****".takeIf { password != null }
return "Credentials(userName=$userName, password=$maskedPassword, certificateAlias=$certificateAlias)"
enum class Type {
UsernamePassword,
OAuth,
ClientCertificate
}
}
\ No newline at end of file
val type: Type
init {
type = when {
!certificateAlias.isNullOrEmpty() ->
Type.ClientCertificate
!userName.isNullOrEmpty() && !accessToken.isNullOrEmpty()
&& !refreshToken.isNullOrEmpty() ->
Type.OAuth
!userName.isNullOrEmpty() && !password.isNullOrEmpty() ->
Type.UsernamePassword
else ->
throw IllegalArgumentException("Invalid account type/credentials")
}
}
override fun toString() =
"Credentials(type=$type, userName=$userName, certificateAlias=$certificateAlias)"
}
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.davdroid.syncadapter
import android.accounts.*
import android.app.Service
import android.content.Context
import android.content.Intent
import android.database.DatabaseUtils
import android.os.Bundle
import androidx.annotation.WorkerThread
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.ui.setup.LoginActivity
import foundation.e.accountmanager.R
import java.util.*
import java.util.logging.Level
import kotlin.concurrent.thread
/**
* Account authenticator for the eelo account type.
*
* Gets started when an eelo account is removed, too, so it also watches for account removals
* and contains the corresponding cleanup code.
*/
class EeloAccountAuthenticatorService : Service(), OnAccountsUpdateListener {
companion object {
fun cleanupAccounts(context: Context) {
Logger.log.info("Cleaning up orphaned accounts")
val accountManager = AccountManager.get(context)
val accountNames = accountManager.getAccountsByType(context.getString(R.string.account_type))
.map { it.name }
// delete orphaned address book accounts
accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
.map { LocalAddressBook(context, it, null) }
.forEach {
try {
if (!accountNames.contains(it.mainAccount.name))
it.delete()
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't delete address book account", e)
}
}
// delete orphaned services in DB
val db = AppDatabase.getInstance(context)
val serviceDao = db.serviceDao()
if (accountNames.isEmpty())
serviceDao.deleteAll()
else
serviceDao.deleteExceptAccounts(accountNames.toTypedArray())
}
}
private lateinit var accountManager: AccountManager
private lateinit var accountAuthenticator: AccountAuthenticator
override fun onCreate() {
accountManager = AccountManager.get(this)
accountManager.addOnAccountsUpdatedListener(this, null, true)
accountAuthenticator = AccountAuthenticator(this)
}
override fun onDestroy() {
super.onDestroy()
accountManager.removeOnAccountsUpdatedListener(this)
}
override fun onBind(intent: Intent?) =
accountAuthenticator.iBinder.takeIf { intent?.action == android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT }
override fun onAccountsUpdated(accounts: Array<out Account>?) {
thread {
cleanupAccounts(this)
}
}
private class AccountAuthenticator(
val context: Context
) : AbstractAccountAuthenticator(context) {
override fun addAccount(response: AccountAuthenticatorResponse?, accountType: String?, authTokenType: String?, requiredFeatures: Array<String>?, options: Bundle?): Bundle {
val intent = Intent(context, LoginActivity::class.java)
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
intent.putExtra(LoginActivity.SETUP_ACCOUNT_PROVIDER_TYPE, LoginActivity.ACCOUNT_PROVIDER_EELO)
val bundle = Bundle(1)
bundle.putParcelable(AccountManager.KEY_INTENT, intent)
return bundle
}
override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?) = null
override fun getAuthTokenLabel(p0: String?) = null
override fun confirmCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Bundle?) = null
override fun updateCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
override fun getAuthToken(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?) = null
override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array<out String>?) = null
}
}
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
import android.accounts.*
import android.app.Service
import android.content.Context
import android.content.Intent
import android.database.DatabaseUtils
import android.os.Bundle
import androidx.annotation.WorkerThread
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.ui.setup.LoginActivity
import foundation.e.accountmanager.R
import java.util.*
import java.util.logging.Level
import kotlin.concurrent.thread
/**
* Account authenticator for the Google account type.
*
* Gets started when a Google account is removed, too, so it also watches for account removals
* and contains the corresponding cleanup code.
*/
class GoogleAccountAuthenticatorService : Service(), OnAccountsUpdateListener {
companion object {
fun cleanupAccounts(context: Context) {
Logger.log.info("Cleaning up orphaned accounts")
val accountManager = AccountManager.get(context)
val accountNames = accountManager.getAccountsByType(context.getString(R.string.account_type))
.map { it.name }
// delete orphaned address book accounts
accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
.map { LocalAddressBook(context, it, null) }
.forEach {
try {
if (!accountNames.contains(it.mainAccount.name))
it.delete()
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't delete address book account", e)
}
}
// delete orphaned services in DB
val db = AppDatabase.getInstance(context)