Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,47 @@ class AuthTokenStorage @Inject constructor(
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

fun saveTokens(accessToken: String, refreshToken: String) {
sharedPreferences.edit()
.putString(KEY_ACCESS_TOKEN, accessToken)
.putString(KEY_REFRESH_TOKEN, refreshToken)
.apply()
}

fun saveAccessToken(accessToken: String) {
sharedPreferences.edit()
.putString(KEY_ACCESS_TOKEN, accessToken)
.apply()
}

fun saveRefreshToken(refreshToken: String) {
sharedPreferences.edit()
.putString(KEY_REFRESH_TOKEN, refreshToken)
.apply()
}

fun getAccessToken(): String? {
return sharedPreferences.getString(KEY_ACCESS_TOKEN, null)
}

fun clearAccessToken() {
fun getRefreshToken(): String? {
return sharedPreferences.getString(KEY_REFRESH_TOKEN, null)
}

fun clearTokens() {
sharedPreferences.edit()
.remove(KEY_ACCESS_TOKEN)
.remove(KEY_REFRESH_TOKEN)
.apply()
}

fun clearAccessToken() {
clearTokens()
}

companion object {
private const val FILE_NAME = "auth_secure_prefs"
private const val KEY_ACCESS_TOKEN = "access_token"
private const val KEY_REFRESH_TOKEN = "refresh_token"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.example.hangsha_android.data.network

import com.example.hangsha_android.data.local.AuthTokenStorage
import com.example.hangsha_android.data.network.api.AuthApi
import com.example.hangsha_android.data.network.model.RefreshTokenRequest
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route

@Singleton
class AuthTokenAuthenticator @Inject constructor(
private val authTokenStorage: AuthTokenStorage,
private val authApi: AuthApi
) : Authenticator {

override fun authenticate(route: Route?, response: Response): Request? {
if (isAuthRequest(response.request) || responseCount(response) >= MAX_AUTH_RETRY_COUNT) {
return null
}

val requestAccessToken = response.request.header(AUTHORIZATION_HEADER)
?.removePrefix(BEARER_PREFIX)
?.trim()
.takeUnless { it.isNullOrBlank() }
?: return null

synchronized(this) {
val latestAccessToken = authTokenStorage.getAccessToken()
if (!latestAccessToken.isNullOrBlank() && latestAccessToken != requestAccessToken) {
return response.request.newBuilder()
.header(AUTHORIZATION_HEADER, "$BEARER_PREFIX$latestAccessToken")
.build()
}

val refreshToken = authTokenStorage.getRefreshToken()
?.takeUnless { it.isBlank() }
?: run {
authTokenStorage.clearTokens()
return null
}

val refreshResponse = runBlocking {
authApi.refresh(
RefreshTokenRequest(refreshToken = refreshToken)
)
}
if (!refreshResponse.isSuccessful) {
authTokenStorage.clearTokens()
return null
}

val refreshedTokens = refreshResponse.body()
val newAccessToken = refreshedTokens?.accessToken
val newRefreshToken = refreshedTokens?.refreshToken
if (newAccessToken.isNullOrBlank() || newRefreshToken.isNullOrBlank()) {
authTokenStorage.clearTokens()
return null
}

authTokenStorage.saveTokens(
accessToken = newAccessToken,
refreshToken = newRefreshToken
)

return response.request.newBuilder()
.header(AUTHORIZATION_HEADER, "$BEARER_PREFIX$newAccessToken")
.build()
}
}

private fun isAuthRequest(request: Request): Boolean {
val path = request.url.encodedPath
return AUTH_PATH_PREFIXES.any(path::startsWith)
}

private fun responseCount(response: Response): Int {
var currentResponse: Response? = response
var count = 1
while (currentResponse?.priorResponse != null) {
count += 1
currentResponse = currentResponse.priorResponse
}
return count
}

private companion object {
private const val AUTHORIZATION_HEADER = "Authorization"
private const val BEARER_PREFIX = "Bearer "
private const val MAX_AUTH_RETRY_COUNT = 2
private val AUTH_PATH_PREFIXES = listOf(
"/api/v1/mobile/auth/",
"/api/v1/auth/"
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ package com.example.hangsha_android.data.network.api

import com.example.hangsha_android.data.network.model.LoginRequest
import com.example.hangsha_android.data.network.model.LoginResponse
import com.example.hangsha_android.data.network.model.RefreshTokenRequest
import com.example.hangsha_android.data.network.model.RegisterRequest
import com.example.hangsha_android.data.network.model.SocialLoginRequest
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST

interface AuthApi {
@POST("api/v1/auth/login")
@POST("api/v1/mobile/auth/login")
suspend fun login(
@Body request: LoginRequest
): Response<LoginResponse>
Expand All @@ -19,8 +20,18 @@ interface AuthApi {
@Body request: SocialLoginRequest
): Response<LoginResponse>

@POST("api/v1/auth/register")
@POST("api/v1/mobile/auth/register")
suspend fun register(
@Body request: RegisterRequest
): Response<LoginResponse>

@POST("api/v1/mobile/auth/refresh")
suspend fun refresh(
@Body request: RefreshTokenRequest
): Response<LoginResponse>

@POST("api/v1/mobile/auth/logout")
suspend fun logout(
@Body request: RefreshTokenRequest
): Response<Unit>
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.hangsha_android.data.network.model

data class LoginResponse(
val accessToken: String
val accessToken: String,
val refreshToken: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.hangsha_android.data.network.model

data class RefreshTokenRequest(
val refreshToken: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.example.hangsha_android.data.repository
import com.example.hangsha_android.data.network.api.AuthApi
import com.example.hangsha_android.data.network.model.LoginRequest
import com.example.hangsha_android.data.network.model.LoginResponse
import com.example.hangsha_android.data.network.model.RefreshTokenRequest
import com.example.hangsha_android.data.network.model.RegisterRequest
import com.example.hangsha_android.data.network.model.SocialLoginRequest
import javax.inject.Inject
Expand Down Expand Up @@ -46,6 +47,18 @@ class AuthRepository @Inject constructor(
)
}

suspend fun logout(refreshToken: String): Response<Unit> {
return authApi.logout(
RefreshTokenRequest(refreshToken = refreshToken)
)
}

suspend fun refresh(refreshToken: String): Response<LoginResponse> {
return authApi.refresh(
RefreshTokenRequest(refreshToken = refreshToken)
)
}

companion object {
private const val GOOGLE_PROVIDER = "GOOGLE"
}
Expand Down
Loading
Loading