Skip to content

Latest commit

 

History

History

android-accountmanager

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

A simple way of calling authenticated requests using retrofit on android

Android Arsenal Android Weekly Snapshot build

Dependencies

  • Retrofit 2.9.0
  • androidx.appcompat 1.3.0
  • kotlin-stdlib 1.5.10

What does it do?

If you call a request method, annotated with the authenticated annotation, it'll do the following steps:

  • Step 1: Checks if there already is an account in the Android AccountManager. If not, it'll open a LoginActivity (you choose which). If there already is an account, go on with step 2, If there's more than one account open an Dialog to pick an account.
  • Step 2: Tries to get the authentication credentials from the (choosen) account for authorizing the request. If there is no valid credential, your LoginActivity will open. After login go to Step 1.
  • Step 3: If no Login was required (credentials exists already), it sends the actual request.
  • Step 4: By implementing a Authenticator you can check the response (i.e. a 401 you will be able to refresh the token) and decide if you want to retry the request or not.

How to use it?

Add it as dependency:

implementation 'com.andretietz.retroauth:android-accountmanager:x.y.z'

Setup

1. Define an Account-Type String

The Account-Type should be unique for an app or company, depending on if you want to share the account in multiple apps of your company or not. I recommend using something like your.company.id.ACCOUNT.

2. Define an Authentication Action.

We'll use this String later in order to start our Login-Activity using an intent-filter in the manifest. This could be something like your.company.id.ACTION

3. Create an Authentication Service.

This Service is started whenever the Android OS is asked for a login of the in #1 provided Account-Type. It then uses the Action-String defined in #2 to show the Login. This must be a service since you can add create accounts within the account-settings.

This is a very small implementation, that could look like this:

class DemoAuthenticationService : AuthenticationService() {
    override fun getLoginAction(): String = "your.company.id.ACTION"
    // optionally to implement. Get's called, when the account will be removed
    override fun cleanupAccount(account: Account) {
        // Here you can trigger your account cleanup (userdata wiping)
        Timber.e("Remove account: ${account.name}")
    }
}

4. Creating the link to the authenticator

With this xml in the res/xml folder of our project we tell the Android OS that there is an authenticator for our Account-Type (defined in #1) If you provide multiple account types, you need to provide multiple authenticator xmls

Here's an example. Make sure you're replacing the accountType with your own.

<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
                   android:accountType="your.company.id.ACCOUNT"
                   android:icon="@mipmap/ic_launcher"
                   android:smallIcon="@mipmap/ic_launcher"
                   android:label="@string/app_name" />

5. Gluing the Service and the Authenticator together.

For that we need to add the Service we created in #3 into the manifest: Note that there's an additional meta-data tag which provides the xml we created in #4. If you have multiple xml's you need to provide additional Services.

     <service
         android:name=".DemoAuthenticationService"
         android:exported="false">
         <intent-filter>
             <action android:name="android.accounts.AccountAuthenticator"/>
         </intent-filter>
         <meta-data
             android:name="android.accounts.AccountAuthenticator"
             android:resource="@xml/authenticator"/>
     </service>

6. Provide a LoginActivity

Make sure you're extending it from the AuthenticationActivity

class LoginActivity : AuthenticationActivity() {
    ...
    fun someLoginMethod() {
        val user: String
        val credential: String
        ...
        // do login work here and make sure, that you provide at least a user and a credential String
        ...
        Account account = createOrGetAccount(user)
        storeCredentials(
                account,
                credentialType,  // AndroidCredentialType
                credential,      // String as you get it from your Authenticator implementation
                mapOf(
                        "some-key" to "some-value"
                )
        // store some additional userdata (optionally)
        storeUserData(account, "key_for_some_user_data", "some-userdata")
        // finishes the activity and set this account to the "current-active" one
        finalizeAuthentication(account)
    }
    ...
}

and add it also in the manifest using a special intent-filter. This intent-filter should as action:name contain the Action String you defined in #2

<?xml version="1.0" encoding="utf-8"?>
<manifest>
...
       <activity android:name=".LoginActivity">
           <intent-filter>
               <action android:name="your.company.id.ACTION"/>
               <category android:name="android.intent.category.DEFAULT"/>
           </intent-filter>
       </activity>
...
</manifest>

Usage

Create an Authenticator implementation

For the Android Implementation you need to create an Authenticator:

class YourAuthenticator
    : Authenticator<String, Account, AndroidCredentialType, AndroidCredentials>() {

There are 3 Methods required to implement:

  • The Owner-Type
    override fun getOwnerType(annotationOwnerType: Int): String

This method provides us the ownerType that has been setup in the @Authenticated annotation. The value is optional! So if you don't need it, don't use it. A reason to use it could be, you need to use multiple ownerTypes on one endpoint.

  • The Credential-Type
    override fun getCredentialType(annotationCredentialType: Int): AndroidCredentialType {
        return AndroidCredentialType(
            "your.company.id.TOKEN_TYPE",
            setOf(
                "some optional",
                "keys",
                "which a credential provides"
            )
        )
    }

Note that when getting an AndroidCredential it only contains the data, which is loaded using this set of optional keys.

  • Authenticate the request
    override fun authenticateRequest(request: Request, credentials: AndroidCredentials): Request {
        return request.newBuilder()
                .header("Authorization", "Bearer " + credentials.token)
                .build()
    }

5. Create your REST interface

interface SomeAuthenticatedService {
 @GET("/some/path")
 fun someUnauthenticatedCall(): Call<ResultObject>

 @Authenticated
 @GET("/some/other/path")
 fun someAuthenticatedCall(): Call<ResultObject>
}
  • Create the Retrofit object and instantiate it
val authenticator: Authenticator = YourAuthenticator() // See Usage
val baseRetrofit: Retrofit = Retrofit.Builder()
        .baseUrl("https://api.awesome.com/")
        // setup your retrofit as you wish...
        .addConverterFactory(GsonConverterFactory.create())
        .build()

// Either this (which also works in plain java)
val authRetrofit: Retrofit = RetroauthAndroid.setup(
    retrofit,
    application,
    authenticator
)
// OR this
val authRetrofit = baseRetrofit.androidAuthentication(application, authenticator)

// create your services
val service = authRetrofit.create(SomeAuthenticatedService.class)
// use them
service.someAuthenticatedCall().execute()

Another option is to create the retrofit instance completely yourself:

// OR if you want to have full control:
val authenticator: Authenticator = YourAuthenticator() // See Usage
val ownerStorage: OwnerStorage<String, Account, AndroidCredentialType> = AndroidAccountManagerOwnerStorage(application)
val credentialStorage: CredentialStorage<Account, AndroidCredentialType, AndroidCredentials> = AndroidAccountManagerCredentialStorage(application)
val retrofit: Retrofit = Retrofit.Builder()
        .baseUrl("https://api.awesome.com/")
        // setup your retrofit as you wish...
        .addConverterFactory(GsonConverterFactory.create())
        .client(
            OkHttpClient.Builder()
                .addInterceptor(CredentialInterceptor(authenticator, ownerStorage, credentialStorage))
                .build()
        )   
        .build()


// create your services
val service = retrofit.create(SomeAuthenticatedService.class)
// use them
service.someAuthenticatedCall().execute()