Reputation: 86
this is a self answered question about adding access token to request header and refresh the token with refresh token, I was struggling with this topic for a long time, and now I'm writing in post hopefully it could help anyone else in same situations
maybe there would be any other better solutions, but it worked for me in the easiest way
Upvotes: 1
Views: 5261
Reputation: 86
in remote module I'm following this method with the help of Hilt:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun providesRetrofit (okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
@Provides
@Singleton
fun providesOkHttpClient(interceptor: AuthInterceptor, authAuthenticator: AuthAuthenticator): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(interceptor)
.authenticator(authAuthenticator)
.build()
}
I send a request to server and receive the access token and refresh token, then I saved them with the power of shared preferences like so:
class TokenManager @Inject constructor(@ApplicationContext context: Context) {
private var prefs: SharedPreferences =
context.getSharedPreferences(PREFS_TOKEN_FILE, Context.MODE_PRIVATE)
fun saveToken(token: UserAuthModel?) {
val editor = prefs.edit()
token?.let {
editor.putString(USER_TOKEN, token.access_token).apply()
editor.putString(USER_REFRESH_TOKEN,token.refresh_token).apply()
editor.putBoolean(IS_LOGGED_IN,true).apply ()
}
}
fun getToken(): String? {
return prefs.getString(USER_TOKEN, null)
}
fun getRefreshToken(): String? {
return prefs.getString(USER_REFRESH_TOKEN, null)
}}
then I use .addInterceptor(interceptor)
in order to add header to all requests like this:
class AuthInterceptor @Inject constructor():Interceptor{
@Inject
lateinit var tokenManager: TokenManager
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
val token = tokenManager.getToken()
request.addHeader("Authorization", "Bearer $token")
request.addHeader("Accept","application/json")
return chain.proceed(request.build())
}}
after that you will have access to every method which requires access token as authentication modo, depending on your API instruction your access token will be expired in a specific time ( maybe 24 hours) and you need a new access token which is accessible with help of refresh token that you already have it, and then I add this line to okHttp .authenticator(authAuthenticator)
when your access token expires, the API will send you back a 401 or 403 error code (it will happen in interceptor section), and in that time Authenticator came into play, luckily it's smart enough to recognize this and do the task,
I take care of Authenticator like this:
class AuthAuthenticator @Inject constructor() : Authenticator {
@Inject lateinit var tokenManager: TokenManager
override fun authenticate(route: Route?, response: Response): Request? {
return runBlocking {
val refreshToken=tokenManager.getRefreshToken()
val refreshTokenR:RequestBody= refreshToken?.toRequestBody() ?: "".toRequestBody()
val grantTypeR:RequestBody= "refresh_token".toRequestBody()
//val newAccessToken = authService.safeRefreshTokenFromApi(refreshToken,grantType)
val newAccessToken = getUpdatedToken(refreshTokenR,grantTypeR)
if (!newAccessToken.isSuccessful){
val intent=Intent(context,MainActivity::class.java)
context.startActivity(intent)
}
tokenManager.saveToken(newAccessToken.body()) // save new access_token for next called
newAccessToken.body()?.let {
response.request.newBuilder()
.header("Authorization", "Bearer ${it.access_token}") // just only need to
override "Authorization" header, don't need to override all header since this new request
is create base on old request
.build()
}
} }
private suspend fun getUpdatedToken( refreshToken:RequestBody,grantType:RequestBody):
retrofit2.Response<UserAuthModel> {
val okHttpClient = OkHttpClient.Builder()
.build()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.build()
val service=retrofit.create(AuthService::class.java)
return service.refreshTokenFromApi(refreshToken,grantType)
}}
Authenticator need to make a request, so it needs a retrofit and OkHttp instance( which will run this very Authenticator),in order to break this cycle I created another instance .
two things I have to mention is :
I guess it's ok to use runBlocking because Authenticator itself is running on another thread
and remember in case of kotlin you have to use suspend function in API service to take care of Unable to create call adapter for retrofit2.Response
error
at the end, I have to mention that I'm using two different API service like this:
general service:
interface MovieService {
@GET("api/v1/movies/{movie-id}")
suspend fun getSingleMovie(@Path("movie-id") movieId:Int):Response<NetworkMovieModel>}
authentication service:
interface AuthService:MovieService {
@Multipart
@POST("oauth/token")
fun refreshTokenFromApi (@Part("refresh_token") username: RequestBody,
@Part("grant_type") grantType: RequestBody
): Response<UserAuthModel>}
Upvotes: 2