Skip to content

Commit

Permalink
[#228] Add token authenticator
Browse files Browse the repository at this point in the history
  • Loading branch information
kaungkhantsoe committed May 28, 2024
1 parent dc5d66c commit fa8f851
Show file tree
Hide file tree
Showing 46 changed files with 972 additions and 34 deletions.
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
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

0 comments on commit fa8f851

Please sign in to comment.