Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#228] Add TokenAuthenticator setup #577

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions template-compose/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ out/

# Local configuration file (sdk path, etc)
local.properties
api-config.properties

# Proguard folder generated by Eclipse
proguard/
Expand Down
1 change: 1 addition & 0 deletions template-compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Clone the project
- Run the project with Android Studio
- Add `api-config.properties` file in the `resources` folder of the :app module to override the default configuration.

## Linter and static code analysis

Expand Down
2 changes: 0 additions & 2 deletions template-compose/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,12 @@ android {
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
signingConfig = signingConfigs[BuildTypes.RELEASE]
buildConfigField("String", "BASE_API_URL", "\"https://jsonplaceholder.typicode.com/\"")
}

debug {
// For quickly testing build with proguard, enable this
isMinifyEnabled = false
signingConfig = signingConfigs[BuildTypes.DEBUG]
buildConfigField("String", "BASE_API_URL", "\"https://jsonplaceholder.typicode.com/\"")
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package co.nimblehq.template.compose.di

import javax.inject.Qualifier

@Qualifier
annotation class Unauthorized

@Qualifier
annotation class Authorized
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package co.nimblehq.template.compose.di.modules

import co.nimblehq.template.compose.util.DispatchersProvider
import co.nimblehq.template.compose.util.DispatchersProviderImpl
import co.nimblehq.template.compose.data.remote.services.ApiService
import co.nimblehq.template.compose.data.repositories.TokenRepositoryImpl
import co.nimblehq.template.compose.data.util.DispatchersProvider
import co.nimblehq.template.compose.data.util.DispatchersProviderImpl
import co.nimblehq.template.compose.domain.repositories.TokenRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import java.util.Properties

@Module
@InstallIn(SingletonComponent::class)
Expand All @@ -14,4 +18,13 @@ class AppModule {
fun provideDispatchersProvider(): DispatchersProvider {
return DispatchersProviderImpl()
}

@Provides
fun provideTokenRepository(
apiService: ApiService,
apiConfigProperties: Properties,
): TokenRepository = TokenRepositoryImpl(
apiService = apiService,
apiConfigProperties = apiConfigProperties,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ package co.nimblehq.template.compose.di.modules

import android.content.Context
import co.nimblehq.template.compose.BuildConfig
import co.nimblehq.template.compose.data.remote.interceptor.AuthInterceptor
import co.nimblehq.template.compose.data.local.preferences.NetworkEncryptedSharedPreferences
import co.nimblehq.template.compose.data.remote.authenticator.RequestAuthenticator
import co.nimblehq.template.compose.data.util.DispatchersProvider
import co.nimblehq.template.compose.di.Authorized
import co.nimblehq.template.compose.di.Unauthorized
import co.nimblehq.template.compose.domain.usecases.GetAuthStatusUseCase
import co.nimblehq.template.compose.domain.usecases.RefreshTokenUseCase
import co.nimblehq.template.compose.domain.usecases.UpdateLoginTokensUseCase
import com.chuckerteam.chucker.api.*
import dagger.Module
import dagger.Provides
Expand All @@ -18,6 +27,7 @@ private const val READ_TIME_OUT = 30L
@InstallIn(SingletonComponent::class)
class OkHttpClientModule {

@Unauthorized
@Provides
fun provideOkHttpClient(
chuckerInterceptor: ChuckerInterceptor
Expand All @@ -31,6 +41,26 @@ class OkHttpClientModule {
}
}.build()

@Authorized
@Provides
fun provideAuthorizedOkHttpClient(
authInterceptor: AuthInterceptor,
chuckerInterceptor: ChuckerInterceptor,
authenticator: RequestAuthenticator,
) = OkHttpClient.Builder().apply {
addInterceptor(authInterceptor)
authenticator(authenticator)
if (BuildConfig.DEBUG) {
addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
addInterceptor(chuckerInterceptor)
readTimeout(READ_TIME_OUT, TimeUnit.SECONDS)
}
}.build().apply {
authenticator.okHttpClient = this
}

@Provides
fun provideChuckerInterceptor(
@ApplicationContext context: Context
Expand All @@ -46,4 +76,31 @@ class OkHttpClientModule {
.alwaysReadResponseBody(true)
.build()
}

@Provides
fun provideAuthInterceptor(
encryptedSharedPreference: NetworkEncryptedSharedPreferences
): AuthInterceptor {
return AuthInterceptor(encryptedSharedPreference)
}

@Provides
fun provideNetworkEncryptedSharedPreferences(
@ApplicationContext context: Context,
): NetworkEncryptedSharedPreferences {
return NetworkEncryptedSharedPreferences(context)
}

@Provides
fun provideRequestAuthenticator(
dispatchersProvider: DispatchersProvider,
getAuthStatusUseCase: GetAuthStatusUseCase,
refreshTokenUseCase: RefreshTokenUseCase,
updateLoginTokensUseCase: UpdateLoginTokensUseCase,
): RequestAuthenticator = RequestAuthenticator(
dispatchersProvider = dispatchersProvider,
getAuthStatusUseCase = getAuthStatusUseCase,
refreshTokenUseCase = refreshTokenUseCase,
updateLoginTokensUseCase = updateLoginTokensUseCase,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStoreFile
import co.nimblehq.template.compose.data.repositories.AppPreferencesRepositoryImpl
import co.nimblehq.template.compose.data.repositories.AuthPreferenceRepositoryImpl
import co.nimblehq.template.compose.domain.repositories.AppPreferencesRepository
import co.nimblehq.template.compose.domain.repositories.AuthPreferenceRepository
import dagger.*
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
Expand All @@ -24,6 +26,11 @@ abstract class PreferencesModule {
appPreferencesRepositoryImpl: AppPreferencesRepositoryImpl
): AppPreferencesRepository

@Binds
abstract fun bindAuthPreferencesRepository(
authPreferenceRepositoryImpl: AuthPreferenceRepositoryImpl
): AuthPreferenceRepository

companion object {
@Singleton
@Provides
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package co.nimblehq.template.compose.di.modules

import co.nimblehq.template.compose.BuildConfig
import co.nimblehq.template.compose.data.remote.providers.*
import co.nimblehq.template.compose.data.remote.services.ApiService
import co.nimblehq.template.compose.data.remote.services.AuthorizedApiService
import co.nimblehq.template.compose.di.Authorized
import co.nimblehq.template.compose.di.Unauthorized
import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
Expand All @@ -11,28 +13,61 @@ import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Converter
import retrofit2.Retrofit
import java.util.Properties

private const val API_CONFIG_PROPERTIES = "api-config.properties"

@Module
@InstallIn(SingletonComponent::class)
class RetrofitModule {

@Provides
fun provideBaseApiUrl() = BuildConfig.BASE_API_URL
fun provideBaseApiUrl(apiConfigProperties: Properties): String =
apiConfigProperties.getProperty("BASE_API_URL")

@Provides
fun provideMoshiConverterFactory(moshi: Moshi): Converter.Factory =
ConverterFactoryProvider.getMoshiConverterFactory(moshi)

@Unauthorized
@Provides
fun provideRetrofit(
baseUrl: String,
okHttpClient: OkHttpClient,
@Unauthorized okHttpClient: OkHttpClient,
converterFactory: Converter.Factory,
): Retrofit = RetrofitProvider
.getRetrofitBuilder(baseUrl, okHttpClient, converterFactory)
.build()

@Authorized
@Provides
fun provideAuthorizedRetrofit(
baseUrl: String,
@Authorized okHttpClient: OkHttpClient,
converterFactory: Converter.Factory,
): Retrofit = RetrofitProvider
.getRetrofitBuilder(baseUrl, okHttpClient, converterFactory)
.build()

@Provides
fun provideApiService(retrofit: Retrofit): ApiService =
fun provideApiService(@Unauthorized retrofit: Retrofit): ApiService =
ApiServiceProvider.getApiService(retrofit)

@Provides
fun provideAuthorizedApiService(@Authorized retrofit: Retrofit): AuthorizedApiService =
ApiServiceProvider.getAuthorizedService(retrofit)

@Provides
fun loadApiConfigProperties(): Properties {
val properties = Properties()
val inputStream = this.javaClass.classLoader?.getResourceAsStream(API_CONFIG_PROPERTIES)
?: throw IllegalArgumentException(
"$API_CONFIG_PROPERTIES file not found. " +
"Please add $API_CONFIG_PROPERTIES in the :app module /resources folder"
)

properties.load(inputStream)

return properties
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import co.nimblehq.template.compose.domain.usecases.UseCase
import co.nimblehq.template.compose.ui.base.BaseViewModel
import co.nimblehq.template.compose.ui.models.UiModel
import co.nimblehq.template.compose.ui.models.toUiModel
import co.nimblehq.template.compose.util.DispatchersProvider
import co.nimblehq.template.compose.data.util.DispatchersProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import javax.inject.Inject
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
BASE_API_URL=BASE_API_URL
CLIENT_ID=CLIENT_ID
CLIENT_SECRET=CLIENT_SECRET
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package co.nimblehq.template.compose.test

import co.nimblehq.template.compose.util.DispatchersProvider
import co.nimblehq.template.compose.data.util.DispatchersProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*
import org.junit.rules.TestWatcher
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import co.nimblehq.template.compose.domain.usecases.UseCase
import co.nimblehq.template.compose.test.CoroutineTestRule
import co.nimblehq.template.compose.test.MockUtil
import co.nimblehq.template.compose.ui.models.toUiModel
import co.nimblehq.template.compose.util.DispatchersProvider
import co.nimblehq.template.compose.data.util.DispatchersProvider
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
Expand Down
2 changes: 1 addition & 1 deletion template-compose/buildSrc/src/main/java/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ object Versions {
const val RETROFIT = "2.9.0"
const val ROBOLECTRIC = "4.10.2"

const val SECURITY_CRYPTO = "1.0.0"
const val SECURITY_CRYPTO = "1.1.0-alpha06"

const val TIMBER = "4.7.1"
const val TURBINE = "0.13.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package co.nimblehq.template.compose.data.local.preferences

import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import java.security.KeyStore

abstract class BaseEncryptedSharedPreferences : BaseSharedPreferences() {

fun deleteExistingPreferences(fileName: String, context: Context) {
context.deleteSharedPreferences(fileName)
}

fun deleteMasterKeyEntry() {
KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
deleteEntry(MasterKey.DEFAULT_MASTER_KEY_ALIAS)
}
}

fun createEncryptedSharedPreferences(fileName: String, context: Context): SharedPreferences {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()

return EncryptedSharedPreferences.create(
context,
fileName,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import android.content.SharedPreferences

abstract class BaseSharedPreferences {

protected lateinit var sharedPreferences: SharedPreferences
lateinit var sharedPreferences: SharedPreferences

protected inline fun <reified T> get(key: String): T? =
inline fun <reified T> get(key: String): T? =
if (sharedPreferences.contains(key)) {
when (T::class) {
Boolean::class -> sharedPreferences.getBoolean(key, false) as T?
Expand All @@ -20,8 +20,8 @@ abstract class BaseSharedPreferences {
null
}

protected fun <T> set(key: String, value: T) {
sharedPreferences.execute {
fun <T> set(key: String, value: T, executeWithCommit: Boolean = false) {
sharedPreferences.execute(executeWithCommit) {
when (value) {
is Boolean -> it.putBoolean(key, value)
is String -> it.putString(key, value)
Expand All @@ -32,11 +32,11 @@ abstract class BaseSharedPreferences {
}
}

protected fun remove(key: String) {
sharedPreferences.execute { it.remove(key) }
fun remove(key: String, executeWithCommit: Boolean = false) {
sharedPreferences.execute(executeWithCommit) { it.remove(key) }
}

protected fun clearAll() {
sharedPreferences.execute { it.clear() }
fun clearAll(executeWithCommit: Boolean = false) {
sharedPreferences.execute(executeWithCommit) { it.clear() }
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
package co.nimblehq.template.compose.data.local.preferences

import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import javax.inject.Inject

private const val APP_SECRET_SHARED_PREFS = "app_secret_shared_prefs"

class EncryptedSharedPreferences @Inject constructor(applicationContext: Context) :
BaseSharedPreferences() {
BaseEncryptedSharedPreferences() {

init {
val masterKey = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
sharedPreferences = EncryptedSharedPreferences.create(
APP_SECRET_SHARED_PREFS,
masterKey,
applicationContext,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
sharedPreferences = createEncryptedSharedPreferences(
fileName = APP_SECRET_SHARED_PREFS,
context = applicationContext
)
}
}
Loading
Loading