- Retrofit 2.9.0
- androidx.appcompat 1.3.0
- kotlin-stdlib 1.5.10
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.
Add it as dependency:
implementation 'com.andretietz.retroauth:android-accountmanager:x.y.z'
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
.
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
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}")
}
}
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" />
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>
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>
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()
}
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()