The purpose of this library is to unify our oauth2 implementation and ease it to integrate with our backend. Still it tries to be as flexible as possible to be able to use it even if some parameters, paths etc is different. The default implementation are using the most common configurations, and the [app] and [core] module contains examples how to integrate the app.
Oauth2 is a protocol for authorization.
In a nutshell for Android developers this means you will have two kinds of request, one with session and one without it.
- The one without session will need to contain a header with a clientId.
- The one with session will need to contain a header with a token.
You get the token when you login / signup. These token can expire, meaning they are no longer usable after a certain amount of time.
When you get the tokens you get two kinds: access token and refresh token:
- The access token has to be attached to the requests.
- The refresh token is used to get a new access token.
When the access token expires you will get 401 Unauthorized from the server, at this point you will need to call a request with the refresh token, from that request you will get a new access and refresh token.
With retrofit there is a class Authenticator which is triggered on a background thread when your request fails with 401. At this point you can call the refresh api and update the request which failed, then it will be retried.
Details can be found here
Blog post explaining it here
The library adds an authenticator implementation to retrofit with session, meaning it will call the refresh request for you. The library adds the proper headers to your requests based on if it were created with session or sessionless retrofit.
Error Cases:
- When the refresh-token is no longer valid or expired, (returned by the server while trying to refresh token) then you will receive a callback with session expiration and your request will fail with 401.
- When the refresh-token request failed 3 times, then your request will fail with 401. And should be handled as normal network error
To see the behaviour in action may refer to [com.halcyonmobile.core.AuthenticationTest]
// top level build.gradle
//..
allprojects {
repositories {
// ...
maven {
url "https://maven.pkg.github.com/halcyonmobile/retrofit-oauth2-helper"
credentials {
username = project.findProperty("GITHUB_USERNAME") ?: System.getenv("GITHUB_USERNAME")
password = project.findProperty("GITHUB_TOKEN") ?: System.getenv("GITHUB_TOKEN")
}
// https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token
}
}
}
// OR
// top level build.gradle.kts
//..
allprojects {
repositories {
// ...
maven {
url = uri("https://maven.pkg.github.com/halcyonmobile/retrofit-oauth2-helper")
credentials {
username = extra.properties["GITHUB_USERNAME"] as String? ?: System.getenv("GITHUB_USERNAME")
password = extra.properties["GITHUB_TOKEN"] as String? ?: System.getenv("GITHUB_TOKEN")
}
// https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token
}
}
}
Note: you only need one maven declaration with "halcyonmobile/{specific}", every other package will be accessible.
- You will see a "core" and "app" module, this is specific to our architecture, the core means a module which does the business logic, network requests etc, it's a java module while the app module is handling the ui and other platform specific implementation details.
- The idea is that in your core module you will do the configuration and get the created retrofit instances so it will depend on either the oauth or oauthkoin, oauthmoshi or some other variant
- However the core module won't be able to contain all the needed dependencies, because of that you should use the oauthdependencies in your app module so you can provide the storage and session expiration handler.
- Optionally you can use the oauthstorage in your app to reduce the shared preferences boilerplate.
- Optionally you can use the oauthadaptergenerator in your core where you define the refresh token retrofit service, so you don't need to write your adapter if it's simple.
- Note: oauth-moshi, oauth-koin do not need adapters, they already contain a refresh service.
If you are using moshi and some other dependency injection framework than koin what you need to do is add the dependency in your build.gradle of your core module Note: still the example will be using koin, to adapt to your DI is your responsibility.
implementation "com.halcyonmobile.oauth-setup:oauth-setup-moshi:latest-version"
Then add the module to your other core modules, the setup will look something like this:
fun createNetworkModules(
clientId: String,
baseUrl: String,
provideAuthenticationLocalStorage: Scope.() -> AuthenticationLocalStorage,
provideSessionExpiredEventHandler: Scope.() -> SessionExpiredEventHandler
): List<Module> {
return listOf(
module {
factory { get<Retrofit>(SESSION_RETROFIT).create(SessionExampleService::class.java) }
factory { get<Retrofit>(NON_SESSION_RETROFIT).create(SessionlessExampleService::class.java) }
factory { ExampleRemoteSource(get(), get()) }
},
module {
single { provideAuthenticationLocalStorage() }
single { provideSessionExpiredEventHandler() }
single {
OauthRetrofitWithMoshiContainerBuilder(
clientId = clientId,
authenticationLocalStorage = provideAuthenticationLocalStorage(),
sessionExpiredEventHandler = provideSessionExpiredEventHandler()
)
.configureRetrofit {
baseUrl(baseUrl)
}
.build()
}
single(SESSION_RETROFIT) { get<OauthRetrofitContainerWithMoshi>().oauthRetrofitContainer.sessionRetrofit }
single(NON_SESSION_RETROFIT) { get<OauthRetrofitContainerWithMoshi>().oauthRetrofitContainer.sessionlessRetrofit }
single { get<OauthRetrofitContainerWithMoshi>().moshi }
}
)
}
If you want to save your session in shared preferences may use oauthstorage, in this case: in your build.gradle of your app module add the following dependency:
implementation "com.halcyonmobile.oauth-setup:oauth-setup-storage:latest-version"
Extend your shared preferences manager from the AuthenticationSharedPreferencesStorage
class SharedPreferenceManager(private val sharedPreferences: SharedPreferences) : AuthenticationSharedPreferencesStorage(sharedPreferences),
Implement the com.halcyonmobile.oauth.depencencies.SessionExpiredEventHandler. And tie the setup together such as:
fun createAllModules(baseUrl: String, clientId: String): List<Module> {
return listOf(createAppModule(omegaApplication))
.plus(createNetworkModules(
baseUrl = baseUrl,
clientId = clientId,
provideAuthenticationLocalStorage = { get<SharedPreferenceManager>() },
provideSessionExpiredEventHandler = { get<SessionExpiredEventHandlerImpl>() }
))
}
Note: if you are not saving your session into shared preferences, instead of 'oauth-setup-storage' dependency, use 'oauth-setup-dependencies'. You implement the AuthenticationLocalStorage interface with your solution and add it to your createNetworkModule setup instead of SharedPreferencesManager.
For your login and signup requests, you still have to save the session yourself into your storage. The easiest solution is return the same session type, inject the AuthenticationLocalStorage and simply call save on it.
Note: There is an idea with call adapter which would save your session automatically, but it's not yet implemented. Feel free to ping me if you are interested in this.
For this there is a specific header which when attached after authentication is finished successfully a specific exception is thrown so you can rerun your request with the updated content.
@GET("test/service")
fun authInvalidTest(
@Header(INVALIDATION_AFTER_REFRESH_HEADER_NAME) invalidHeader : String = INVALIDATION_AFTER_REFRESH_HEADER_VALUE
) : Call<Unit>
// throws authFinishedInvalidationException, which is an IOException after authentication happened
How to handle the exception:
fun foo(){
runCatchingCausedByAuthFinishedInvalidation({
service.authInvalidTest()
}, {
// authentication happened, the storage is updated
// do something, like retrying the request with updated body
})
}
If you are using moshi and some other dependency injection framework than koin what you need to do is add the dependency in your build.gradle of your core module Note: still the example will be using koin, to adapt to your DI is your responsibility.
implementation "com.halcyonmobile.oauth-setup:oauth-setup-gson:latest-version"
Then add the module to your other core modules, the setup will look something like this:
fun createNetworkModules(
clientId: String,
baseUrl: String,
provideAuthenticationLocalStorage: Scope.() -> AuthenticationLocalStorage,
provideSessionExpiredEventHandler: Scope.() -> SessionExpiredEventHandler
): List<Module> {
return listOf(
module {
factory { get<Retrofit>(SESSION_RETROFIT).create(SessionExampleService::class.java) }
factory { get<Retrofit>(NON_SESSION_RETROFIT).create(SessionlessExampleService::class.java) }
factory { ExampleRemoteSource(get(), get()) }
},
module {
single { provideAuthenticationLocalStorage() }
single { provideSessionExpiredEventHandler() }
single {
OauthRetrofitWithGsonContainerBuilder(
clientId = clientId,
authenticationLocalStorage = provideAuthenticationLocalStorage(),
sessionExpiredEventHandler = provideSessionExpiredEventHandler()
)
.configureRetrofit {
baseUrl(baseUrl)
}
.build()
}
single(SESSION_RETROFIT) { get<OauthRetrofitContainerWithGson>().oauthRetrofitContainer.sessionRetrofit }
single(NON_SESSION_RETROFIT) { get<OauthRetrofitContainerWithGson>().oauthRetrofitContainer.sessionlessRetrofit }
single { get<OauthRetrofitContainerWithGson>().gson }
}
)
}
Same as Oauth-moshi setup, please check that one out.
If you are using koin with moshi what you need to do is add the dependency in your build.gradle of your core module
implementation "com.halcyonmobile.oauth-setup:oauth-setup-moshi-koin:latest-version"
Then add the module to your other core modules, the setup will look something like this:
fun createNetworkModules(
clientId: String,
baseUrl: String,
provideAuthenticationLocalStorage: Scope.() -> AuthenticationLocalStorage,
provideSessionExpiredEventHandler: Scope.() -> SessionExpiredEventHandler
): List<Module> {
return listOf(
// your own custom module,
module {
factory { get<Retrofit>(SESSION_RETROFIT).create(SessionExampleService::class.java) }
factory { get<Retrofit>(NON_SESSION_RETROFIT).create(SessionlessExampleService::class.java) }
factory { ExampleRemoteSource(get(), get()) }
},
// this returns a koin module. here you can customize the setup.
createOauthModule(
clientId = clientId,
provideSessionExpiredEventHandler = provideSessionExpiredEventHandler,
provideAuthenticationLocalStorage = provideAuthenticationLocalStorage,
configureRetrofit = {
it.baseUrl(baseUrl)
}
)
)
}
Same as Oauth-moshi setup, please check that one out.
If none of the other setups are applicable, you are not using moshi then you can fallback to this, however i would suggest to add a new module with your implementation instead. Note: still the example will be using koin and moshi, to adapt to your DI is your responsibility.
implementation "com.halcyonmobile.oauth-setup:oauth-setup:latest-version"
// optional
implementation "com.halcyonmobile.oauth-setup:oauth-adapter-generator:latest-version"
Create your DTO for the session, example using moshi:
@JsonClass(generateAdapter = true)
data class RefreshTokenResponsex(
@field:Json(name = "user_id") override val userId: String,
@field:Json(name = "access_token") override val token: String,
@field:Json(name = "refresh_token") override val refreshToken: String,
@field:Json(name = "token_type") override val tokenType: String
) : SessionDataResponse
Create your refresh token service, example of it:
@RefreshService // optional, needed if you use the oauth-adapter-generator
interface RefreshTokenService {
@POST("oauth/token")
@FormUrlEncoded
fun refresh(@Field("refresh_token") refreshToken: String, @Field("grant_type") grantType: String = "refresh_token"): Call<RefreshTokenResponsex>
}
If you choose not to use the annotation processor or you are unable to because of some customization, you will have to create your own adapter Example what the annotation processor generates:
/**
* [AuthenticationServiceAdapter] implementation generated for
[com.halcyonmobile.core.RefreshTokenService] class annotated with
[com.halcyonmobile.oauth.dependencies.RefreshService] */
internal class RefreshTokenServiceAuthenticationServiceAdapter :
AuthenticationServiceAdapter<RefreshTokenService> {
override fun adapt(service: RefreshTokenService): AuthenticationService =
AuthenticationServiceImpl(service)
class AuthenticationServiceImpl(private val service: RefreshTokenService) :
AuthenticationService {
override fun refreshToken(refreshToken: String): Call<out SessionDataResponse> =
service.refresh(refreshToken)
}
}
Then add the module to your other core modules, the setup will look something like this:
fun createNetworkModules(
clientId: String,
baseUrl: String,
provideAuthenticationLocalStorage: Scope.() -> AuthenticationLocalStorage,
provideSessionExpiredEventHandler: Scope.() -> SessionExpiredEventHandler
): List<Module> {
return listOf(
module {
factory { get<Retrofit>(SESSION_RETROFIT).create(SessionExampleService::class.java) }
factory { get<Retrofit>(NON_SESSION_RETROFIT).create(SessionlessExampleService::class.java) }
factory { ExampleRemoteSource(get(), get()) }
},
module {
single { provideAuthenticationLocalStorage() }
single { provideSessionExpiredEventHandler() }
single { Moshi.Builder().build() }
single {
OauthRetrofitContainerBuilder(
clientId = clientId,
refreshServiceClass = RefreshTokenService::class,
authenticationLocalStorage = provideAuthenticationLocalStorage(),
sessionExpiredEventHandler = provideSessionExpiredEventHandler(),
adapter = RefreshTokenServiceAuthenticationServiceAdapter()
)
.configureRetrofit {
baseUrl(baseUrl).addConverterFactory(MoshiConverterFactory.create(get()))
}
.build()
}
single(SESSION_RETROFIT) { get<OauthRetrofitContainer>().sessionRetrofit }
single(NON_SESSION_RETROFIT) { get<OauthRetrofitContainer>().sessionlessRetrofit }
}
)
}
Same as oauth-moshi-koin setup, please check that one out.
For any kind of configuration there is a function in the builder class. To see more specifically, please refer the documentation of the used builder.
The basic ones are the following:
- What should be considered SessionExpiration
- Configure the okhttp (both, sessionless, session), adding logger, timeout changes etc.
- Configure the retrofit, adding baseurl, parser etc.
For parsers there are more configuration:
- Configuring the moshi or gson
- Configuring the service path used
- Configuring the grantType value
- Configuring the parameter name of the refresh token send with the refresh token request
- disabling the default parsing (in this case the user is responsibil for the parsing)
- add addinitonal parameters to the refresh token service
The following section describes current modules and the preferred content & usage
- An example how can you include this into your app
- An example of a core layer which used for networking and how it needs to configure the retrofit instances
- The base implementation of the extension.
- Dependencies which has to come from the outside (app module), but has no relation to retrofit
- this module contains configuration functions which create the koin modules you can simply add to your startKoin method
- uses koin 1.0.2
- WIP
- A persistent storage for session based on SharedPreferences. It's implemented in a way that can be used separately or with an existing SharedPreferencesManager
- A persistent storage for session based on EncryptedSharedPreferences. It's implemented in a way that can be used separately or with an existing SharedPreferencesManager
- min API 23
- A persistent storage for session based on EncryptedSharedPreferences above API 23 and SharedPreferences below. It's implemented in a way that can be used separately or with an existing SharedPreferencesManager
- An optional annotation processor which tries to reduce boilerplate even more. You may use it if you don't use a version which does the parsing for you.
- An extension of the base implementation which includes the service and interfaces for other modules which do the actual parsing of the session.
- An extension of the base implementation which includes moshi and the service with default parameters Here you don't need to write your own service and parsing, however you are still able to configure the service and parsing.
Note: it implements the oauthparsing
- this module contains a configuration function which create the koin module, which you can simply add to your other modules
- uses koin 2.0.1
- An extension of the base implementation which includes gson and the service with default parameters Here you don't need to write your own service and parsing, however you are still able to configure the service and parsing.
Note: it implements the oauthparsing
Copyright (c) 2020 Halcyon Mobile.
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
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.