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:
- This would have meant adding three new permissions to both the apps. The new permissions being
GET_ACCOUNTS
,AUTHENTICATE_ACCOUNTS
andMANAGE_ACCOUNTS
. Gotta be careful with permissions!. - Having to create an Authenticator - we already have our own comprehensive
OAuth
logic handling the authentication. Don’t fix it if isn’t broken! - Having to implement a new Service, thinking about some extra use-cases and a lot of boilerplate code.
- Would have had to implement and link a new login UI with AccountManager, in case a user would have liked to create an account directly from the Account Manager.
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
- The requesting application requests login information.
- A content resolver queries the device for any provider application that can provide login credentials.
- 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.