Skip to content

Commit fe479f7

Browse files
committed
[#228] Add token authenticator
1 parent dc5d66c commit fe479f7

File tree

47 files changed

+973
-34
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+973
-34
lines changed

template-compose/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ out/
2222

2323
# Local configuration file (sdk path, etc)
2424
local.properties
25+
api-config.properties
2526

2627
# Proguard folder generated by Eclipse
2728
proguard/

template-compose/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

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

1011
## Linter and static code analysis
1112

template-compose/app/build.gradle.kts

-2
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,12 @@ android {
5959
isShrinkResources = true
6060
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
6161
signingConfig = signingConfigs[BuildTypes.RELEASE]
62-
buildConfigField("String", "BASE_API_URL", "\"https://jsonplaceholder.typicode.com/\"")
6362
}
6463

6564
debug {
6665
// For quickly testing build with proguard, enable this
6766
isMinifyEnabled = false
6867
signingConfig = signingConfigs[BuildTypes.DEBUG]
69-
buildConfigField("String", "BASE_API_URL", "\"https://jsonplaceholder.typicode.com/\"")
7068
}
7169
}
7270

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package co.nimblehq.template.compose.di
2+
3+
import javax.inject.Qualifier
4+
5+
@Qualifier
6+
annotation class Unauthorized
7+
8+
@Qualifier
9+
annotation class Authorized
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package co.nimblehq.template.compose.di.modules
22

3-
import co.nimblehq.template.compose.util.DispatchersProvider
4-
import co.nimblehq.template.compose.util.DispatchersProviderImpl
3+
import co.nimblehq.template.compose.data.remote.services.ApiService
4+
import co.nimblehq.template.compose.data.repositories.TokenRepositoryImpl
5+
import co.nimblehq.template.compose.data.util.DispatchersProvider
6+
import co.nimblehq.template.compose.data.util.DispatchersProviderImpl
7+
import co.nimblehq.template.compose.domain.repositories.TokenRepository
58
import dagger.Module
69
import dagger.Provides
710
import dagger.hilt.InstallIn
811
import dagger.hilt.components.SingletonComponent
12+
import java.util.Properties
913

1014
@Module
1115
@InstallIn(SingletonComponent::class)
@@ -14,4 +18,13 @@ class AppModule {
1418
fun provideDispatchersProvider(): DispatchersProvider {
1519
return DispatchersProviderImpl()
1620
}
21+
22+
@Provides
23+
fun provideTokenRepository(
24+
apiService: ApiService,
25+
apiConfigProperties: Properties,
26+
): TokenRepository = TokenRepositoryImpl(
27+
apiService = apiService,
28+
apiConfigProperties = apiConfigProperties,
29+
)
1730
}

template-compose/app/src/main/java/co/nimblehq/template/compose/di/modules/OkHttpClientModule.kt

+57
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ package co.nimblehq.template.compose.di.modules
22

33
import android.content.Context
44
import co.nimblehq.template.compose.BuildConfig
5+
import co.nimblehq.template.compose.data.remote.interceptor.AuthInterceptor
6+
import co.nimblehq.template.compose.data.local.preferences.NetworkEncryptedSharedPreferences
7+
import co.nimblehq.template.compose.data.remote.authenticator.RequestAuthenticator
8+
import co.nimblehq.template.compose.data.util.DispatchersProvider
9+
import co.nimblehq.template.compose.di.Authorized
10+
import co.nimblehq.template.compose.di.Unauthorized
11+
import co.nimblehq.template.compose.domain.usecases.GetAuthStatusUseCase
12+
import co.nimblehq.template.compose.domain.usecases.RefreshTokenUseCase
13+
import co.nimblehq.template.compose.domain.usecases.UpdateLoginTokensUseCase
514
import com.chuckerteam.chucker.api.*
615
import dagger.Module
716
import dagger.Provides
@@ -18,6 +27,7 @@ private const val READ_TIME_OUT = 30L
1827
@InstallIn(SingletonComponent::class)
1928
class OkHttpClientModule {
2029

30+
@Unauthorized
2131
@Provides
2232
fun provideOkHttpClient(
2333
chuckerInterceptor: ChuckerInterceptor
@@ -31,6 +41,26 @@ class OkHttpClientModule {
3141
}
3242
}.build()
3343

44+
@Authorized
45+
@Provides
46+
fun provideAuthorizedOkHttpClient(
47+
authInterceptor: AuthInterceptor,
48+
chuckerInterceptor: ChuckerInterceptor,
49+
authenticator: RequestAuthenticator,
50+
) = OkHttpClient.Builder().apply {
51+
addInterceptor(authInterceptor)
52+
authenticator(authenticator)
53+
if (BuildConfig.DEBUG) {
54+
addInterceptor(HttpLoggingInterceptor().apply {
55+
level = HttpLoggingInterceptor.Level.BODY
56+
})
57+
addInterceptor(chuckerInterceptor)
58+
readTimeout(READ_TIME_OUT, TimeUnit.SECONDS)
59+
}
60+
}.build().apply {
61+
authenticator.okHttpClient = this
62+
}
63+
3464
@Provides
3565
fun provideChuckerInterceptor(
3666
@ApplicationContext context: Context
@@ -46,4 +76,31 @@ class OkHttpClientModule {
4676
.alwaysReadResponseBody(true)
4777
.build()
4878
}
79+
80+
@Provides
81+
fun provideAuthInterceptor(
82+
encryptedSharedPreference: NetworkEncryptedSharedPreferences
83+
): AuthInterceptor {
84+
return AuthInterceptor(encryptedSharedPreference)
85+
}
86+
87+
@Provides
88+
fun provideNetworkEncryptedSharedPreferences(
89+
@ApplicationContext context: Context,
90+
): NetworkEncryptedSharedPreferences {
91+
return NetworkEncryptedSharedPreferences(context)
92+
}
93+
94+
@Provides
95+
fun provideRequestAuthenticator(
96+
dispatchersProvider: DispatchersProvider,
97+
getAuthStatusUseCase: GetAuthStatusUseCase,
98+
refreshTokenUseCase: RefreshTokenUseCase,
99+
updateLoginTokensUseCase: UpdateLoginTokensUseCase,
100+
): RequestAuthenticator = RequestAuthenticator(
101+
dispatchersProvider = dispatchersProvider,
102+
getAuthStatusUseCase = getAuthStatusUseCase,
103+
refreshTokenUseCase = refreshTokenUseCase,
104+
updateLoginTokensUseCase = updateLoginTokensUseCase,
105+
)
49106
}

template-compose/app/src/main/java/co/nimblehq/template/compose/di/modules/PreferencesModule.kt

+7
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory
66
import androidx.datastore.preferences.core.Preferences
77
import androidx.datastore.preferences.preferencesDataStoreFile
88
import co.nimblehq.template.compose.data.repositories.AppPreferencesRepositoryImpl
9+
import co.nimblehq.template.compose.data.repositories.AuthPreferenceRepositoryImpl
910
import co.nimblehq.template.compose.domain.repositories.AppPreferencesRepository
11+
import co.nimblehq.template.compose.domain.repositories.AuthPreferenceRepository
1012
import dagger.*
1113
import dagger.hilt.InstallIn
1214
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -24,6 +26,11 @@ abstract class PreferencesModule {
2426
appPreferencesRepositoryImpl: AppPreferencesRepositoryImpl
2527
): AppPreferencesRepository
2628

29+
@Binds
30+
abstract fun bindAuthPreferencesRepository(
31+
authPreferenceRepositoryImpl: AuthPreferenceRepositoryImpl
32+
): AuthPreferenceRepository
33+
2734
companion object {
2835
@Singleton
2936
@Provides
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package co.nimblehq.template.compose.di.modules
22

3-
import co.nimblehq.template.compose.BuildConfig
43
import co.nimblehq.template.compose.data.remote.providers.*
54
import co.nimblehq.template.compose.data.remote.services.ApiService
5+
import co.nimblehq.template.compose.data.remote.services.AuthorizedApiService
6+
import co.nimblehq.template.compose.di.Authorized
7+
import co.nimblehq.template.compose.di.Unauthorized
68
import com.squareup.moshi.Moshi
79
import dagger.Module
810
import dagger.Provides
@@ -11,28 +13,61 @@ import dagger.hilt.components.SingletonComponent
1113
import okhttp3.OkHttpClient
1214
import retrofit2.Converter
1315
import retrofit2.Retrofit
16+
import java.util.Properties
17+
18+
private const val API_CONFIG_PROPERTIES = "api-config.properties"
1419

1520
@Module
1621
@InstallIn(SingletonComponent::class)
1722
class RetrofitModule {
1823

1924
@Provides
20-
fun provideBaseApiUrl() = BuildConfig.BASE_API_URL
25+
fun provideBaseApiUrl(apiConfigProperties: Properties): String =
26+
apiConfigProperties.getProperty("BASE_API_URL")
2127

2228
@Provides
2329
fun provideMoshiConverterFactory(moshi: Moshi): Converter.Factory =
2430
ConverterFactoryProvider.getMoshiConverterFactory(moshi)
2531

32+
@Unauthorized
2633
@Provides
2734
fun provideRetrofit(
2835
baseUrl: String,
29-
okHttpClient: OkHttpClient,
36+
@Unauthorized okHttpClient: OkHttpClient,
37+
converterFactory: Converter.Factory,
38+
): Retrofit = RetrofitProvider
39+
.getRetrofitBuilder(baseUrl, okHttpClient, converterFactory)
40+
.build()
41+
42+
@Authorized
43+
@Provides
44+
fun provideAuthorizedRetrofit(
45+
baseUrl: String,
46+
@Authorized okHttpClient: OkHttpClient,
3047
converterFactory: Converter.Factory,
3148
): Retrofit = RetrofitProvider
3249
.getRetrofitBuilder(baseUrl, okHttpClient, converterFactory)
3350
.build()
3451

3552
@Provides
36-
fun provideApiService(retrofit: Retrofit): ApiService =
53+
fun provideApiService(@Unauthorized retrofit: Retrofit): ApiService =
3754
ApiServiceProvider.getApiService(retrofit)
55+
56+
@Provides
57+
fun provideAuthorizedApiService(@Authorized retrofit: Retrofit): AuthorizedApiService =
58+
ApiServiceProvider.getAuthorizedService(retrofit)
59+
60+
@Provides
61+
fun loadApiConfigProperties(): Properties {
62+
val properties = Properties()
63+
val inputStream = this.javaClass.classLoader?.getResourceAsStream(API_CONFIG_PROPERTIES)
64+
?: throw IllegalArgumentException(
65+
"$API_CONFIG_PROPERTIES file not found. " +
66+
"Please add $API_CONFIG_PROPERTIES in the :app module /resources folder"
67+
)
68+
69+
properties.load(inputStream)
70+
71+
return properties
72+
}
3873
}

template-compose/app/src/main/java/co/nimblehq/template/compose/ui/screens/main/home/HomeViewModel.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import co.nimblehq.template.compose.domain.usecases.UseCase
55
import co.nimblehq.template.compose.ui.base.BaseViewModel
66
import co.nimblehq.template.compose.ui.models.UiModel
77
import co.nimblehq.template.compose.ui.models.toUiModel
8-
import co.nimblehq.template.compose.util.DispatchersProvider
8+
import co.nimblehq.template.compose.data.util.DispatchersProvider
99
import dagger.hilt.android.lifecycle.HiltViewModel
1010
import kotlinx.coroutines.flow.*
1111
import javax.inject.Inject
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
BASE_API_URL=BASE_API_URL
2+
CLIENT_ID=CLIENT_ID
3+
CLIENT_SECRET=CLIENT_SECRET

template-compose/app/src/test/java/co/nimblehq/template/compose/test/CoroutineTestRule.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package co.nimblehq.template.compose.test
22

3-
import co.nimblehq.template.compose.util.DispatchersProvider
3+
import co.nimblehq.template.compose.data.util.DispatchersProvider
44
import kotlinx.coroutines.*
55
import kotlinx.coroutines.test.*
66
import org.junit.rules.TestWatcher

template-compose/app/src/test/java/co/nimblehq/template/compose/ui/screens/main/home/HomeViewModelTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import co.nimblehq.template.compose.domain.usecases.UseCase
55
import co.nimblehq.template.compose.test.CoroutineTestRule
66
import co.nimblehq.template.compose.test.MockUtil
77
import co.nimblehq.template.compose.ui.models.toUiModel
8-
import co.nimblehq.template.compose.util.DispatchersProvider
8+
import co.nimblehq.template.compose.data.util.DispatchersProvider
99
import io.kotest.matchers.shouldBe
1010
import io.mockk.every
1111
import io.mockk.mockk

template-compose/buildSrc/src/main/java/Versions.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ object Versions {
4242
const val RETROFIT = "2.9.0"
4343
const val ROBOLECTRIC = "4.10.2"
4444

45-
const val SECURITY_CRYPTO = "1.0.0"
45+
const val SECURITY_CRYPTO = "1.1.0-alpha06"
4646

4747
const val TIMBER = "4.7.1"
4848
const val TURBINE = "0.13.0"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package co.nimblehq.template.compose.data.local.preferences
2+
3+
import android.content.Context
4+
import android.content.SharedPreferences
5+
import androidx.security.crypto.EncryptedSharedPreferences
6+
import androidx.security.crypto.MasterKey
7+
import java.security.KeyStore
8+
9+
abstract class BaseEncryptedSharedPreferences : BaseSharedPreferences() {
10+
11+
fun deleteExistingPreferences(fileName: String, context: Context) {
12+
context.deleteSharedPreferences(fileName)
13+
}
14+
15+
fun deleteMasterKeyEntry() {
16+
KeyStore.getInstance("AndroidKeyStore").apply {
17+
load(null)
18+
deleteEntry(MasterKey.DEFAULT_MASTER_KEY_ALIAS)
19+
}
20+
}
21+
22+
fun createEncryptedSharedPreferences(fileName: String, context: Context): SharedPreferences {
23+
val masterKey = MasterKey.Builder(context)
24+
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
25+
.build()
26+
27+
return EncryptedSharedPreferences.create(
28+
context,
29+
fileName,
30+
masterKey,
31+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
32+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
33+
)
34+
}
35+
}

template-compose/data/src/main/java/co/nimblehq/template/compose/data/local/preferences/BaseSharedPreferences.kt

+8-8
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import android.content.SharedPreferences
44

55
abstract class BaseSharedPreferences {
66

7-
protected lateinit var sharedPreferences: SharedPreferences
7+
lateinit var sharedPreferences: SharedPreferences
88

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

23-
protected fun <T> set(key: String, value: T) {
24-
sharedPreferences.execute {
23+
fun <T> set(key: String, value: T, executeWithCommit: Boolean = false) {
24+
sharedPreferences.execute(executeWithCommit) {
2525
when (value) {
2626
is Boolean -> it.putBoolean(key, value)
2727
is String -> it.putString(key, value)
@@ -32,11 +32,11 @@ abstract class BaseSharedPreferences {
3232
}
3333
}
3434

35-
protected fun remove(key: String) {
36-
sharedPreferences.execute { it.remove(key) }
35+
fun remove(key: String, executeWithCommit: Boolean = false) {
36+
sharedPreferences.execute(executeWithCommit) { it.remove(key) }
3737
}
3838

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

33
import android.content.Context
4-
import androidx.security.crypto.EncryptedSharedPreferences
5-
import androidx.security.crypto.MasterKeys
64
import javax.inject.Inject
75

86
private const val APP_SECRET_SHARED_PREFS = "app_secret_shared_prefs"
97

108
class EncryptedSharedPreferences @Inject constructor(applicationContext: Context) :
11-
BaseSharedPreferences() {
9+
BaseEncryptedSharedPreferences() {
1210

1311
init {
14-
val masterKey = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
15-
sharedPreferences = EncryptedSharedPreferences.create(
16-
APP_SECRET_SHARED_PREFS,
17-
masterKey,
18-
applicationContext,
19-
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
20-
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
12+
sharedPreferences = createEncryptedSharedPreferences(
13+
fileName = APP_SECRET_SHARED_PREFS,
14+
context = applicationContext
2115
)
2216
}
2317
}

0 commit comments

Comments
 (0)