Shared login across Android apps

A little background

Our two flagship Android apps - Bodyweight and Nutrition serve two very different needs but usually to the same target audience. An essential feature missing in the user experience so far was to share the login across the two apps so that users who were signed into one of these apps could be automatically signed into the other. Cool, right? We tackled this issue some time back and implemented a way to share login between our two main apps.

We started by looking at Android’s Account Manager which seemed like the natural choice. However, we eventually steered away from it. We looked at some other possible implementations too and in the end we went ahead with a Content Provider solution. Let’s go over the possible implementations that we considered before we look at our Content Provider solution.

Possible alternate solutions

Account Manager

Account Manager is the consolidated API from Android for handling everything related to user’s online accounts. This did seem like the natural choice for our usecase but without going into a lot of detail here is why we thought this isn’t the best solution for us:

AIDL - Android Interface Definition Language

AIDL is the way interprocess communication takes place on Android. For two processes to talk to each other, the complex objects are broken down into primitives and marshaled across processes.

Using android:process

This is an attribute for the application tag in the AndroidManifest. This can be set to share one process with another application. This only works if the two applications are signed with the same certificate.

Shared Login using Content Provider

Content Provider

We used ContentProviders for achieving the shared login across the two apps. The application looking for login credentials is the requesting application. This request will be answered by an application that contains the ContentProvider - this is our provider application.

Flow

  1. The requesting application requests login information.
  2. A content resolver queries the device for any provider application that can provide login credentials.
  3. On successful resolution, the provider application responds to this request by returning the login information.

Duality

Our codebase is a modularised monolith for the two apps in question. We implemented this solution as yet another feature module. Any app that includes this module becomes a provider application as well as a requesting application. This ensures scalability in case we need to include more applications to this exchange.

Content Resolution

When the requesting application is trying to find a provider application, it queries for a specific content scheme and authority.

URI Authority

The name of the authority that provides the data has to be declared in the Android Manifest. Typically, this includes the name of the application and then the name of the ContentProvider subclass. This name is used by the requesting application to find the correct content provider. Our module declares this by using build variables so that the app including this module auto-populates this field and becomes the provider application. Elegance.

<provider
	android:name= ...
	android:authorities="${applicationId}.login.provider"
	android:exported="true"
	android:permission= ... 
/>

Custom Permissions

Our Content Provider defines a custom permission with android:protectionLevel=“signature” so that only the applications signed by the same signing key can access this provider.

<permission
	android:name= ...
	android:protectionLevel="signature" 
/>

Release and Debug environments

Another feature that we achieved was to make sure debug and release apps do not end up sharing login credentials with each other. We specified different permissions for release and debug apps. This is important because production and debug environments correspond to different backend instances and mixing them up would have lead to unexpected results. Again, this was seamless as our module declared two AndroidManifest files under debug/ and release/ buildTypes respectively.

Sharing login content

After the requesting application has established that there is a provider application, we can go ahead and share some login data securely. In our case, a simple data class represents the data we share between the two apps. We use the refresh token to request for the access token and then the requesting app can continue to perform its tasks.

data class SharedData(val userId: String, val refreshToken: RefreshToken)

Code

Using the points discussed above, we arrived at our solution which looks something like the following;

AndroidManifest.xml

...
<!-- Using permission to behave like a requesting app -->
<uses-permission android:name="com.foo.bar.permission.LOGIN_PROVIDER" />

<!-- Declaring permission to behave like a provider app-->
<permission
    android:name="com.foo.bar.permission.LOGIN_PROVIDER"
    android:protectionLevel="signature" />

...

<provider
	android:name="com.foo.bar.contentprovider.LoginContentProvider"
    android:authorities="${applicationId}.login.provider"
    android:exported="true"
    android:permission="com.foo.bar.permission.LOGIN_PROVIDER" />

...

LoginContentProvider.kt

class LoginContentProvider : ContentProvider() {

	// Use the query method to provide login data
	override fun query(
		...
	): Cursor? {
		...
	}
	...
}

Furthermore, we have an interface that the apps use directly to request login that hides all the details of the actual sharing.

interface SharedLoginRequests {
    fun requestLogin(): SharedData?
}

Content providers let us control the permission levels for accessing data and with this secure data exchange, we were able to successfully implement shared login between our apps.