Skip to content

Commit 8a867f0

Browse files
committed
[#228] Add TokenAuthenticator to template-xml
1 parent 66b5f5d commit 8a867f0

File tree

19 files changed

+328
-21
lines changed

19 files changed

+328
-21
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package co.nimblehq.template.xml.di
2+
3+
import javax.inject.Qualifier
4+
5+
@Qualifier
6+
@Retention(AnnotationRetention.BINARY)
7+
annotation class Authenticate
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package co.nimblehq.template.xml.di.modules
2+
3+
import co.nimblehq.template.xml.data.repository.SessionManagerImpl
4+
import co.nimblehq.template.xml.data.repository.TokenRefresherImpl
5+
import co.nimblehq.template.xml.data.service.AuthService
6+
import co.nimblehq.template.xml.data.service.SessionManager
7+
import co.nimblehq.template.xml.data.service.authenticator.TokenRefresher
8+
import dagger.Module
9+
import dagger.Provides
10+
import dagger.hilt.InstallIn
11+
import dagger.hilt.components.SingletonComponent
12+
13+
@Module
14+
@InstallIn(SingletonComponent::class)
15+
class AuthModule {
16+
17+
@Provides
18+
fun provideAuthService(authService: AuthService): TokenRefresher = TokenRefresherImpl(authService)
19+
20+
@Provides
21+
fun provideSessionManager(): SessionManager = SessionManagerImpl()
22+
}

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

+40-10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ package co.nimblehq.template.xml.di.modules
22

33
import android.content.Context
44
import co.nimblehq.template.xml.BuildConfig
5+
import co.nimblehq.template.xml.data.service.SessionManager
6+
import co.nimblehq.template.xml.data.service.authenticator.ApplicationRequestAuthenticator
7+
import co.nimblehq.template.xml.data.service.authenticator.TokenRefresher
8+
import co.nimblehq.template.xml.di.Authenticate
59
import com.chuckerteam.chucker.api.*
610
import dagger.Module
711
import dagger.Provides
@@ -20,16 +24,42 @@ class OkHttpClientModule {
2024

2125
@Provides
2226
fun provideOkHttpClient(
23-
chuckerInterceptor: ChuckerInterceptor
24-
) = OkHttpClient.Builder().apply {
25-
if (BuildConfig.DEBUG) {
26-
addInterceptor(HttpLoggingInterceptor().apply {
27-
level = HttpLoggingInterceptor.Level.BODY
28-
})
29-
addInterceptor(chuckerInterceptor)
30-
readTimeout(READ_TIME_OUT, TimeUnit.SECONDS)
31-
}
32-
}.build()
27+
chuckerInterceptor: ChuckerInterceptor,
28+
sessionManager: SessionManager,
29+
tokenRefresher: TokenRefresher?
30+
): OkHttpClient {
31+
val authenticator =
32+
tokenRefresher?.let { ApplicationRequestAuthenticator(it, sessionManager) }
33+
return OkHttpClient.Builder()
34+
.apply {
35+
if (BuildConfig.DEBUG) {
36+
addInterceptor(HttpLoggingInterceptor().apply {
37+
level = HttpLoggingInterceptor.Level.BODY
38+
})
39+
addInterceptor(chuckerInterceptor)
40+
readTimeout(READ_TIME_OUT, TimeUnit.SECONDS)
41+
}
42+
}
43+
.build()
44+
.apply { authenticator?.okHttpClient = this }
45+
46+
}
47+
48+
@Authenticate
49+
@Provides
50+
fun provideAuthOkHttpClient(chuckerInterceptor: ChuckerInterceptor): OkHttpClient {
51+
return OkHttpClient.Builder()
52+
.apply {
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+
}
61+
.build()
62+
}
3363

3464
@Provides
3565
fun provideChuckerInterceptor(

template-xml/app/src/main/java/co/nimblehq/template/xml/di/modules/RetrofitModule.kt

+19
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package co.nimblehq.template.xml.di.modules
22

33
import co.nimblehq.template.xml.BuildConfig
44
import co.nimblehq.template.xml.data.service.ApiService
5+
import co.nimblehq.template.xml.data.service.AuthService
56
import co.nimblehq.template.xml.data.service.providers.ApiServiceProvider
67
import co.nimblehq.template.xml.data.service.providers.ConverterFactoryProvider
78
import co.nimblehq.template.xml.data.service.providers.RetrofitProvider
9+
import co.nimblehq.template.xml.di.Authenticate
810
import com.squareup.moshi.Moshi
911
import dagger.Module
1012
import dagger.Provides
@@ -37,4 +39,21 @@ class RetrofitModule {
3739
@Provides
3840
fun provideApiService(retrofit: Retrofit): ApiService =
3941
ApiServiceProvider.getApiService(retrofit)
42+
43+
44+
@Authenticate
45+
@Provides
46+
fun provideAuthRetrofit(
47+
baseUrl: String,
48+
@Authenticate okHttpClient: OkHttpClient,
49+
converterFactory: Converter.Factory,
50+
): Retrofit = RetrofitProvider
51+
.getRetrofitBuilder(baseUrl, okHttpClient, converterFactory)
52+
.build()
53+
54+
@Provides
55+
fun provideAuthService(
56+
@Authenticate retrofit: Retrofit
57+
): AuthService =
58+
ApiServiceProvider.getAuthService(retrofit)
4059
}

template-xml/data/src/main/java/co/nimblehq/template/xml/data/extensions/ResponseMapping.kt

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package co.nimblehq.template.xml.data.extensions
22

33
import co.nimblehq.template.xml.data.response.ErrorResponse
4-
import co.nimblehq.template.xml.data.response.toModel
4+
import co.nimblehq.template.xml.data.response.toAuthenticated
55
import co.nimblehq.template.xml.data.service.providers.MoshiBuilderProvider
66
import co.nimblehq.template.xml.domain.exceptions.ApiException
77
import co.nimblehq.template.xml.domain.exceptions.NoConnectivityException
@@ -33,7 +33,7 @@ private fun Throwable.mapError(): Throwable {
3333
is HttpException -> {
3434
val errorResponse = parseErrorResponse(response())
3535
ApiException(
36-
errorResponse?.toModel(),
36+
errorResponse?.toAuthenticated(),
3737
code(),
3838
message()
3939
)
@@ -42,7 +42,7 @@ private fun Throwable.mapError(): Throwable {
4242
}
4343
}
4444

45-
private fun parseErrorResponse(response: Response<*>?): ErrorResponse? {
45+
fun parseErrorResponse(response: Response<*>?): ErrorResponse? {
4646
val jsonString = response?.errorBody()?.string()
4747
return try {
4848
val moshi = MoshiBuilderProvider.moshiBuilder.build()
@@ -54,3 +54,15 @@ private fun parseErrorResponse(response: Response<*>?): ErrorResponse? {
5454
null
5555
}
5656
}
57+
58+
fun parseErrorResponse(jsonString: String?): ErrorResponse? {
59+
return try {
60+
val moshi = MoshiBuilderProvider.moshiBuilder.build()
61+
val adapter = moshi.adapter(ErrorResponse::class.java)
62+
adapter.fromJson(jsonString.orEmpty())
63+
} catch (exception: IOException) {
64+
null
65+
} catch (exception: JsonDataException) {
66+
null
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package co.nimblehq.template.xml.data.repository
2+
3+
import co.nimblehq.template.xml.data.response.AuthenticateResponse
4+
import co.nimblehq.template.xml.data.service.SessionManager
5+
6+
class SessionManagerImpl: SessionManager {
7+
8+
override suspend fun getAccessToken(): String {
9+
TODO("Not yet implemented")
10+
}
11+
12+
override suspend fun getRefreshToken(): String {
13+
TODO("Not yet implemented")
14+
}
15+
16+
override suspend fun getRegistrationToken(): String {
17+
TODO("Not yet implemented")
18+
}
19+
20+
override suspend fun getTokenType(): String {
21+
TODO("Not yet implemented")
22+
}
23+
24+
override suspend fun refresh(authenticateResponse: AuthenticateResponse) {
25+
TODO("Not yet implemented")
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package co.nimblehq.template.xml.data.repository
2+
3+
import co.nimblehq.template.xml.data.extensions.flowTransform
4+
import co.nimblehq.template.xml.data.response.AuthenticateResponse
5+
import co.nimblehq.template.xml.data.service.AuthService
6+
import co.nimblehq.template.xml.data.service.authenticator.TokenRefresher
7+
import kotlinx.coroutines.flow.Flow
8+
9+
class TokenRefresherImpl constructor(
10+
private val authService: AuthService
11+
) : TokenRefresher {
12+
13+
override suspend fun refreshToken(): Flow<AuthenticateResponse> = flowTransform {
14+
authService.refreshToken()
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package co.nimblehq.template.xml.data.response
2+
3+
import co.nimblehq.template.xml.domain.model.AuthStatus
4+
import com.squareup.moshi.Json
5+
6+
data class AuthenticateResponse(
7+
@Json(name = "access_token")
8+
val accessToken: String,
9+
@Json(name = "refresh_token")
10+
val refreshToken: String,
11+
@Json(name = "status")
12+
val status: String,
13+
@Json(name = "token_type")
14+
val tokenType: String?
15+
)
16+
17+
fun AuthenticateResponse.toAuthenticated() = AuthStatus.Authenticated(
18+
accessToken = accessToken,
19+
refreshToken = refreshToken,
20+
status = status,
21+
tokenType = tokenType
22+
)

template-xml/data/src/main/java/co/nimblehq/template/xml/data/response/ErrorResponse.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import com.squareup.moshi.Json
55

66
data class ErrorResponse(
77
@Json(name = "message")
8-
val message: String
8+
val message: String,
9+
@Json(name = "type")
10+
val type: String?
911
)
1012

11-
internal fun ErrorResponse.toModel() = Error(message = message)
13+
internal fun ErrorResponse.toAuthenticated() = Error(message = message, type = type)

template-xml/data/src/main/java/co/nimblehq/template/xml/data/response/Response.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ data class Response(
77
@Json(name = "id") val id: Int?
88
)
99

10-
private fun Response.toModel() = Model(id = this.id)
10+
private fun Response.toAuthenticated() = Model(id = this.id)
1111

12-
fun List<Response>.toModels() = this.map { it.toModel() }
12+
fun List<Response>.toModels() = this.map { it.toAuthenticated() }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package co.nimblehq.template.xml.data.service
2+
3+
import co.nimblehq.template.xml.data.response.AuthenticateResponse
4+
import retrofit2.http.POST
5+
6+
interface AuthService {
7+
8+
@POST("refreshToken")
9+
suspend fun refreshToken(): AuthenticateResponse
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package co.nimblehq.template.xml.data.service
2+
3+
import co.nimblehq.template.xml.data.response.AuthenticateResponse
4+
5+
interface SessionManager {
6+
7+
suspend fun getAccessToken(): String
8+
9+
suspend fun getRefreshToken(): String
10+
11+
suspend fun getRegistrationToken(): String
12+
13+
suspend fun getTokenType(): String
14+
15+
suspend fun refresh(authenticateResponse: AuthenticateResponse)
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package co.nimblehq.template.xml.data.service.authenticator
2+
3+
import android.annotation.SuppressLint
4+
import android.util.Log
5+
import co.nimblehq.template.xml.data.extensions.parseErrorResponse
6+
import co.nimblehq.template.xml.data.service.SessionManager
7+
import co.nimblehq.template.xml.domain.exceptions.NoConnectivityException
8+
import kotlinx.coroutines.*
9+
import kotlinx.coroutines.flow.last
10+
import okhttp3.*
11+
12+
const val REQUEST_HEADER_AUTHORIZATION = "Authorization"
13+
14+
class ApplicationRequestAuthenticator(
15+
private val tokenRefresher: TokenRefresher,
16+
private val sessionManager: SessionManager
17+
) : Authenticator {
18+
19+
lateinit var okHttpClient: OkHttpClient
20+
21+
private var retryCount = 0
22+
23+
@SuppressLint("CheckResult", "LongMethod", "TooGenericExceptionCaught")
24+
override fun authenticate(route: Route?, response: Response): Request? =
25+
runBlocking {
26+
if (shouldSkipAuthenticationByErrorType(response)) {
27+
return@runBlocking null
28+
}
29+
30+
// Due to unable to check the last retry succeeded
31+
// So reset the retry count on the request first triggered by an automatic retry
32+
if (response.priorResponse == null && retryCount != 0) {
33+
retryCount = 0
34+
}
35+
36+
if (retryCount >= MAX_ATTEMPTS) {
37+
// Reset retry count once reached max attempts
38+
retryCount = 0
39+
return@runBlocking null
40+
} else {
41+
retryCount++
42+
43+
val tokenType = sessionManager.getTokenType()
44+
val failedAccessToken = sessionManager.getAccessToken()
45+
46+
try {
47+
val refreshTokenResponse = tokenRefresher.refreshToken().last().copy(
48+
// refreshToken response doesn't send tokenType
49+
tokenType = tokenType
50+
)
51+
val newAccessToken = refreshTokenResponse.accessToken
52+
53+
if (newAccessToken.isEmpty() || newAccessToken == failedAccessToken) {
54+
// Avoid infinite loop if the new Token == old (failed) token
55+
return@runBlocking null
56+
}
57+
58+
// Update the Interceptor (for future requests)
59+
sessionManager.refresh(refreshTokenResponse)
60+
61+
// Retry this failed request (401) with the new token
62+
return@runBlocking response.request
63+
.newBuilder()
64+
.header(REQUEST_HEADER_AUTHORIZATION, "$tokenType $newAccessToken")
65+
.build()
66+
} catch (e: Exception) {
67+
Log.w("AUTHENTICATOR", "Failed to refresh token: $e")
68+
return@runBlocking if (e !is NoConnectivityException) {
69+
// cancel all pending requests
70+
okHttpClient.dispatcher.cancelAll()
71+
response.request
72+
} else {
73+
// do nothing
74+
null
75+
}
76+
}
77+
}
78+
}
79+
80+
private fun shouldSkipAuthenticationByErrorType(response: Response): Boolean {
81+
val headers = response.request.headers
82+
val skippingError = headers[HEADER_AUTHENTICATION_SKIPPING_ERROR_TYPE]
83+
84+
if (!skippingError.isNullOrEmpty()) {
85+
// Clone response body
86+
// https://github.com/square/okhttp/issues/1240#issuecomment-330813274
87+
val responseBody = response.peekBody(Long.MAX_VALUE).toString()
88+
val error = parseErrorResponse(responseBody)
89+
90+
return error != null && skippingError == error.type
91+
}
92+
return false
93+
}
94+
}
95+
96+
const val HEADER_AUTHENTICATION_SKIPPING_ERROR_TYPE = "Authentication-Skipping-ErrorType"
97+
private const val MAX_ATTEMPTS = 3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package co.nimblehq.template.xml.data.service.authenticator
2+
3+
import co.nimblehq.template.xml.data.response.AuthenticateResponse
4+
import kotlinx.coroutines.flow.Flow
5+
6+
interface TokenRefresher {
7+
8+
suspend fun refreshToken(): Flow<AuthenticateResponse>
9+
}

0 commit comments

Comments
 (0)