How can I refresh my token using authenticator? I need the refresh token method to return the token or null when I get 401 in my api call.
class SupportInterceptor() : Interceptor, Authenticator {
/**
* Interceptor class for setting of the headers for every request
*/
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
request = request?.newBuilder()
?.addHeader("Content-Type", "application/json")
?.addHeader("app-id", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
?.build()
return chain.proceed(request)
}
/**
* Returns a request that includes a credential to satisfy an authentication challenge in
* [response]. Returns null if the challenge cannot be satisfied.
*
* The route is best effort, it currently may not always be provided even when logically
* available. It may also not be provided when an authenticator is re-used manually in an
* application interceptor, such as when implementing client-specific retries.
*/
override fun authenticate(route: Route?, response: Response): Request? {
var requestAvailable: Request? = null
try {
return runBlocking {
when (val tokenResponse = refreshToken()) {
is Success -> {
// userPreferences.saveAccessTokens(
// tokenResponse.value.access_token!!,
// tokenResponse.value.refresh_token!!
// )
response.request.newBuilder()
.header("Authorization", "Bearer ${tokenResponse.value.access_token}")
.build()
}
else -> null
}
}
// requestAvailable = response?.request?.newBuilder()
//// ?.addHeader("Authorization", "Bearer $token")
// ?.build()
// return requestAvailable
} catch (ex: Exception) {
}
return requestAvailable
}
suspend fun refreshToken(): Either<Failure, String?> {
return withContext(Dispatchers.IO) {
try {
val PREFS_NAME = "userPref"
val sharedPref: SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val refreshToken = sharedPref.getString(MyConstants.KEY_REFRESH_TOKEN, "")
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(baseURL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val api: TokenRefreshApi = retrofit.create(TokenRefreshApi::class.java)
val response = api.refreshAccessToken(refreshToken).execute()
// val call: Call<LogIn> = api.refreshAccessToken(refreshToken)
when (response.isSuccessful) {
false -> Either.Left(response.errorResponse())
true -> {
val editor: SharedPreferences.Editor = sharedPref.edit()
editor.putString(
MyConstants.KEY_ACCESS_TOKEN,
response.body()!!.access_token
)
editor.putString(
MyConstants.KEY_REFRESH_TOKEN,
response.body()!!.refresh_token
)
editor!!.apply()
response.body()!!.access_token
}
}
} catch (e: Exception) {
Timber.e("searchTasks: $e")
Either.Left(Failure.UnknownError)
}
}
}
}
I would first clarify the industry standard behavior for reliable clients:
Client tries an API request with an access token
If client receives a 401 it attempts to silently refresh the access token and retry the API request with the new token
If there are technical problems avoid redirecting the user to re-authenticate
Here is some plain Kotlin code of mine that does this.
Looks to me like Retrofit's Authenticator interface makes this easier, and will do the retry for you. Your code looks mostly correct and similar to mine but without the manual checks for 401s:
You need to test 401s though, and one way to do this is to add arbitrary characters to an access token during development, to simulate expiry
In case someone is having difficulty same like me.
override fun authenticate(route: Route?, response: Response): Request? {
var requestAvailable: Request? = null
try {
return runBlocking {
val tokenResponse = getNewToken()
if (!tokenResponse.isNullOrEmpty()) {
response.request.newBuilder()
.header("Authorization", "Bearer ${tokenResponse}")
.build()
} else {
null
}
}
} catch (ex: Exception) {
}
return requestAvailable
}
private fun getNewToken(): String? {
val PREFS_NAME = "userPref"
val sharedPref: SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val refreshToken: String = sharedPref.getString("refresh_token", "").orEmpty()
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_API_URL_CONSUMER)
.addConverterFactory(GsonConverterFactory.create())
.build()
val hashMap = HashMap<String, String>()
hashMap.put("refresh_token", refreshToken)
val call = retrofit.create(TokenRefreshApi::class.java).refreshAccessToken(hashMap)
val authTokenResponse = call?.execute()?.body()
if (authTokenResponse != null) {
val editor: SharedPreferences.Editor = sharedPref.edit()
editor.putString(
"access_token",
authTokenResponse!!.access_token
)
editor.putString(
"refresh_token",
authTokenResponse!!.refresh_token
)
editor!!.apply()
return authTokenResponse!!.access_token
} else {
return null
}
}
Related
I am developing an android application that requires authentication (Login).
When a user logs in, A bearer token jwt, is generated from my backend
I store this token using SharedPreference
Using Retrofit OkHttp Interceptor, I add an interceptor that requires this saved bearer token in order to access authorized endpoints.
The problem is
When a user logs in for the first time, and visit any authorised endpoint, it throws a 401 - UnAuthorized. The user have to close the app, then open it (Without logging in) the endpoints start working, Returning 200. (Meanwhile, I did a check and the bearer token is been sent and is saved to the app) but for some reason for the first time, the endpoints returns 401. Unless, I close the app and open it, then the endpoints throws 200.
The backend guys says its from my app, because the bearer token is sent and it works first time on Post Man.
The backend uses NodeJs.
LoginActivity
if (it.data?.status == "success") {
//get the token and store it
val token = it.data.token
//store token
PreferenceHelper.storeToken = token
//store user session
PreferenceHelper.isLoggedInSeller = true
//go to home activity
val intent = Intent(this, SellerHomeActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
startActivity(intent)
}
PreferenceHelper
object PreferenceHelper {
private const val NAME = "session"
private const val MODE = Context.MODE_PRIVATE
private lateinit var preferences: SharedPreferences
//SharedPreferences variables
private val hasLoggedInSeller = Pair("isLoginSeller", false)
private val loginToken = Pair("login_token", "")
fun init(context: Context) {
preferences = context.getSharedPreferences(NAME, MODE)
}
//an inline function to put variable and save it
private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {
val editor = edit()
operation(editor)
editor.apply()
}
//check if seller has logged in
var isLoggedInSeller: Boolean
get() = preferences.getBoolean(hasLoggedInSeller.first, hasLoggedInSeller.second)
set(value) = preferences.edit {
it.putBoolean(hasLoggedInSeller.first, value)
}
//store login token for buyer
var storeToken: String?
get() = preferences.getString(loginToken.first, loginToken.second)
set(value) = preferences.edit {
it.putString(loginToken.first, value)
}
}
AuthInterceptor
class AuthInterceptor : Interceptor {
var token = PreferenceHelper.storeToken
override fun intercept(chain: Interceptor.Chain): Response {
val requestBuilder = chain.request().newBuilder()
// If token has been saved, add it to the request
token?.let {
requestBuilder.addHeader("Authorization", "Bearer $it")
}
return chain.proceed(requestBuilder.build())
}
}
Retrofit
#Provides
#Singleton
fun provideRetrofit(gson: Gson) : Retrofit = Retrofit.Builder()
.baseUrl(EndPoints.BASE_URL)
.client(
OkHttpClient.Builder().also { client ->
val logging = HttpLoggingInterceptor()
if (BuildConfig.DEBUG) {
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
}
client.addInterceptor(logging)
client.addInterceptor(AuthInterceptor())
client.connectTimeout(120, TimeUnit.SECONDS)
client.readTimeout(120, TimeUnit.SECONDS)
client.protocols(Collections.singletonList(Protocol.HTTP_1_1))
}.build()
)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
Logout
//remove session and token
PreferenceHelper.storeToken = ""
PreferenceHelper.isLoggedInSeller = false
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
startActivity(intent)
Problem is in Interceptor, move the token statement inside intercept method which will be triggered for every request else it would only be created once per instance and will continue to use the same token
class AuthInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val requestBuilder = chain.request().newBuilder()
PreferenceHelper.storeToken?.let {
requestBuilder.addHeader("Authorization", "Bearer $it")
}
return chain.proceed(requestBuilder.build())
}
}
This is the code I have for the refreshing token in an android app using kotlin ad retrofit 2.
The gradle :
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-moshi:retrofit:2.9.0"
And Authenticator is :
class OAuthAuthenticator(
private val refreshTokenService: Repository,
private val sessionManager: SessionManager
) : Authenticator {
#Synchronized
override fun authenticate(route: Route?, response: Response): Request? {
try {
//synchronized call to refresh the token
val refreshTokenResponse =
refreshTokenService.refreshJWTToken(sessionManager.getAuthTokens())
val sessionDataResponseBody = refreshTokenResponse.body()
if (refreshTokenResponse.isSuccessful && sessionDataResponseBody != null && !sessionDataResponseBody.jwt.isNullOrEmpty()) {
sessionManager.jwtToken = sessionDataResponseBody.jwt
// retry request with the new tokens (I get 400 error)
return response.request()
.newBuilder()
.addHeader("Authorization", "Bearer ${sessionManager.jwtToken}")
.build()
} else {
throw HttpException(refreshTokenResponse)
}
} catch (throwable: Throwable) {
when (throwable) {
is HttpException -> {
onSessionExpiration()
return null
}
}
}
return null
}
private fun onSessionExpiration() {
sessionManager.clear()
}
}
This is the Repository class :
object Repository {
fun refreshJWTToken(authTokens : AuthTokens) = RetrofitBuilder.userApi.getAuthenticationToken(authTokens).execute()
}
This is the API :
interface UserAPI {
#Headers("Cache-Control: no-cache")
#POST(AUTH_TOKENS_URL)
fun getAuthenticationToken(
#Header("Accept") accept : String,
#Header("Content-Type") contentType : String,
#Body params: AuthTokens
): Call<AuthTokenResponse>
}
The retrofit builder:
init {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val sessionManager = SessionManager.getInstance(context)
val httpLoggingInterceptor =
HttpLoggingInterceptor()
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
httpClient = OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(ConnectivityCheckInterceptor(connectivityManager))
.addInterceptor(AuthInterceptor(sessionManager))
.authenticator(OAuth2Authenticator(UserRepository, sessionManager))
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
.build()
}
Question :
I can confirm that the code refreshes the Auth token and persists it successfully. However I get a 400 error after that. Any suggestions on what I am doing wrong?
I know this question is old, but for everyone who facing the same issue, it was just a simple mistake.
Please, use header(..., ...) instead of addHeader(..., ...) in the TokenAuthenticator class.
It worked for me.
I am trying to use the Authenticator to handle 401 response. What I have done is
fun provideAccessTokenAuthenticator(
mainApiServiceHolder: MainApiServiceHolder,
preferences: SharedPreferences
) = object : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
val accessToken = preferences.getString(ACCESS_TOKEN, null)
if (!isRequestWithAccessToken(response) || accessToken == null) {
return null
}
synchronized(this) {
val newAccessToken = preferences.getString(ACCESS_TOKEN, null)!!
// Access token is refreshed in another thread.
if (accessToken != newAccessToken) {
return newRequestWithAccessToken(response.request, newAccessToken)
}
// Need to refresh an access token
val refreshTokenResponse = runBlocking {
Log.d("zzzzzzzzzz", "refresh token is running")
mainApiServiceHolder.mainApiService?.refreshToken(
"refresh_token",
preferences.getString(REFRESH_TOKEN, null)!!,
AuthRepository.CLIENT_ID,
AuthRepository.CLIENT_SECRET
)
}
Log.d("zzzzzzzzzz", refreshTokenResponse?.body()?.access_token!!)
return if (refreshTokenResponse?.isSuccessful!!) {
Log.d("zzzzzzzzzz", "refresh token is successful")
newRequestWithAccessToken(
response.request,
refreshTokenResponse.body()?.access_token!!
)
} else {
Log.d("zzzzzzzzzz", "refresh token is unsuccessful")
response.request.newBuilder().header("Content-Type", "application/json").build()
}
}
}
Now, it gets called when there is a 401 response. The refresh token call is also fired (from Log). However, it never gets the result in the refreshTokenResponse and nothing happens after that. I think its a wrong way of using runBlock. The api is
#FormUrlEncoded
#POST("/api/auth/token/")
suspend fun refreshToken(
#Field("grant_type") grant_type: String,
#Field("refresh_token") refresh_token: String,
#Field("client_id") client_id: String,
#Field("client_secret") client_secret: String
): Response<LoginResponse>
Any help would be really appreciated. Thanks
In the Retrofit API, consider replacing your async runBlocking{} suspend fun with a synchronous Call. I had the most luck avoiding the use of coroutines inside the Authenticator.
I was having the same problem. The token request went straight into a black hole. The app froze. The request was never seen again. No error, no nothing.
But everywhere else in the app, the suspend fun came back just fine. From ViewModels, from WorkManager, it worked every time. But from the Authenticator, never. What was wrong with the Authenticator? What was special about the Authenticator that made it act this way?
Then I replaced the runBlocking{} coroutine with a straightforward Call. This time, the request came back and the token arrived without a fuss.
The way I got the API to work looked like this:
#FormUrlEncoded
#POST("token")
fun refreshTokenSync(
#Field("refresh_token") refreshToken: String,
): Call<RefreshMyTokenResponse>
Then, in the Authenticator:
val call = API.refreshTokenSync(refreshToken)
val response = call.execute().body()
I hope this helps someone else who ran into the same issue. You may receive a warning from Android Studio that this is an inappropriate blocking call. Ignore it.
Refresh token only once for multiple requests
Log out user if refreshToken failed
Log out if user gets an error after first refreshing
Queue all requests while token is being refreshed
https://github.com/hoc081098/Refresh-Token-Sample/blob/master/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt
class AuthInterceptor #Inject constructor(
private val userLocalSource: UserLocalSource,
private val apiService: Provider<ApiService>,
) : Interceptor {
private val mutex = Mutex()
override fun intercept(chain: Interceptor.Chain): Response {
val req = chain.request().also { Timber.d("[1] $it") }
if (NO_AUTH in req.headers.values(CUSTOM_HEADER)) {
return chain.proceedWithToken(req, null)
}
val token =
runBlocking { userLocalSource.user().first() }?.token.also { Timber.d("[2] $req $it") }
val res = chain.proceedWithToken(req, token)
if (res.code != HTTP_UNAUTHORIZED || token == null) {
return res
}
Timber.d("[3] $req")
val newToken: String? = runBlocking {
mutex.withLock {
val user =
userLocalSource.user().first().also { Timber.d("[4] $req $it") }
val maybeUpdatedToken = user?.token
when {
user == null || maybeUpdatedToken == null -> null.also { Timber.d("[5-1] $req") } // already logged out!
maybeUpdatedToken != token -> maybeUpdatedToken.also { Timber.d("[5-2] $req") } // refreshed by another request
else -> {
Timber.d("[5-3] $req")
val refreshTokenRes =
apiService.get().refreshToken(RefreshTokenBody(user.refreshToken, user.username))
.also {
Timber.d("[6] $req $it")
}
val code = refreshTokenRes.code()
if (code == HTTP_OK) {
refreshTokenRes.body()?.token?.also {
Timber.d("[7-1] $req")
userLocalSource.save(
user.toBuilder()
.setToken(it)
.build()
)
}
} else if (code == HTTP_UNAUTHORIZED) {
Timber.d("[7-2] $req")
userLocalSource.save(null)
null
} else {
Timber.d("[7-3] $req")
null
}
}
}
}
}
return if (newToken !== null) chain.proceedWithToken(req, newToken) else res
}
private fun Interceptor.Chain.proceedWithToken(req: Request, token: String?): Response =
req.newBuilder()
.apply {
if (token !== null) {
addHeader("Authorization", "Bearer $token")
}
}
.removeHeader(CUSTOM_HEADER)
.build()
.let(::proceed)
}
I am using Authenticator instead of Interceptor to refresh the token. I am able to detect the 401 exception and easily refresh the new token. Everything is working perfectly but the issue is following:
I am unable to call the request again, I do not want the user to hit again to call the offer.
So after execution of the code below I get a new token, it gives me a 401 error message.
My Question is: How can I call the request chain again?
Any advice on the implementation is welcome.
class OffersViewModel
val observable = ApiServiceClient.createApiUsingToken(context).getOffers(
Pref.getString(getApplication(), Pref.CUSTOMER_CODE, "")!!,
Pref.getString(getApplication(), Pref.TOKEN, "")!!
)
compositeDisposable.add(observable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
responseModel.statusCode = StatusCode.START
offersRegisteredUserResponseLiveData.postValue(responseModel)
}
.subscribe({ success ->
if (success.errors.isNullOrEmpty()) {
success.statusCode = StatusCode.SUCCESS
} else {
success.statusCode = StatusCode.ERROR
}
offersRegisteredUserResponseLiveData.value = success
}, {
//HERE I GOT 401
Log.d("debug",it.message.toString())
responseModel.statusCode = StatusCode.ERROR
offersRegisteredUserResponseLiveData.value = responseModel
}, { })
)
API Service Class
/*.....Offer Screen...........*/
#GET("offers/xyz/{abc}")
fun getOffers(
#Path("abc") customerCode: String,
#Header("Authorization") authorization: String,
#Header("Content-Type") contentType: String = CONTENT_TYPE
):
Observable<OfferRegisteredUserResponseModel>
ApiClient Class
fun createApiUsingToken(context: Context?): ApiService {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val client = OkHttpClient.Builder().addInterceptor(interceptor).connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.authenticator(TokenInterceptor(context)).build()
val retrofit = Retrofit.Builder()
.client(client)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(Constants.BASE_URL)
.build()
var ApiServiceClient=retrofit.create(ApiService::class.java)
return retrofit.create(ApiService::class.java)
}
class TokenInterceptor
var requestAvailable: Request? = null
if (response!!.code() === 401) {
var retrofitResponse = ApiServiceClient.createToken().getTokenWithoutObserver().execute()
if (retrofitResponse != null) {
val refreshTokenResponse = retrofitResponse!!.body()
val newAccessToken = refreshTokenResponse!!.token
if (newAccessToken != null)
{
Pref.setString(MyApplication.mInstance, Pref.TOKEN, "${refreshTokenResponse.tokenType} ${refreshTokenResponse?.token}")
Pref.setString(MyApplication.mInstance, Pref.TOKEN_EXPIRES_IN, refreshTokenResponse.tokenExpirationTime.toString())
Utils.addTokenExpirationTimeToCurrentTime(MyApplication.mInstance, refreshTokenResponse.tokenExpirationTime?.toInt()!!)
try {
requestAvailable = response?.request()?.newBuilder()
?.addHeader("Content-Type", "application/json")
?.addHeader("Authorization", "Bearer " + newAccessToken)
?.build()
return requestAvailable
} catch (ex: Exception) {
}
}
} else
return null
}
return requestAvailable
Couple of things i see wrong with this.
First is that even if you "restart" the request with the new token, if you happen to make another request while the "new token" is not saved, that request is also going to fail.
Second is that i don't see that you save the new token anywhere (in SharedPrefs for example for later use).
This is how i would have do it: (preferenceHelper is SharedPrefs)
override fun authenticate(route: Route?, response: Response): Request? {
val HEADER_AUTHORIZATION = "Authorization"
// We need to have a token in order to refresh it.
val token = preferenceHelper.getAccessToken() ?: return null
synchronized(this) {
val newToken = preferenceHelper.getAccessToken() ?: return null
// Check if the request made was previously made as an authenticated request.
if (response.request().header(HEADER_AUTHORIZATION) != null) {
// If the token has changed since the request was made, use the new token.
if (newToken != token) {
return response.request()
.newBuilder()
.removeHeader(HEADER_AUTHORIZATION)
.addHeader(HEADER_AUTHORIZATION, "Bearer " + newToken)
.build()
}
val tokenResponse = ApiServiceClient.createToken().getTokenWithoutObserver().execute()
if (tokenResponse.isSuccessful) {
val userToken = tokenResponse.body() ?: return null
preferenceHelper.saveAccessToken(userToken.token)
preferenceHelper.saveRefreshToken(userToken.refreshToken)
// Retry the request with the new token.
return response.request()
.newBuilder()
.removeHeader(HEADER_AUTHORIZATION)
.addHeader(HEADER_AUTHORIZATION, "Bearer " + userToken.token)
.build()
} else {
logoutUser()
}
}
}
return null
}
I'm getting a 401, because token has expired, so I need to renew the token with another call and then do the call again, is there an easy way instead of doing :
disposable = loginService.login(
UserToLoginRequest(
input_email_login.text.toString(),
input_password_login.text.toString(),
generateRandomDeviceInfo()
)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result ->
//It works
},
{ error -> if(error.code == 401) renewAccessToken() }
)
The thing is that I want to do something like this guy : Refreshing Oath token, but if it's possible to call again the same endpoint with the same parameters.
Example
getApple(1) <-- return info of apple id 1
The result is 401 <-- can't do the call without refreshing the accessToken refreshAccessToken()
Automatically call getApple(1) without disturbing the user
class Inceptor : Interceptor {
internal var token: String?=null
#Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
var request=chain.request()
request=request.newBuilder().build()
val response=chain.proceed(request)
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
// get a new token (I use a synchronous Retrofit call)
val requestBody=FormBody.Builder()
.add("UserName", “abcd")
.add("Password", “*****")
.build()
val newRequest=request.newBuilder()
.url("Put your url")
.post(requestBody)
.build()
val tokenRefreshResponse=chain.proceed(newRequest)
val newRetryHttpUrl=request.url()
if (tokenRefreshResponse.code() == HttpURLConnection.HTTP_OK) {
val retryOriginaResponseBody: RequestBody
val builder=FormBody.Builder()
retryOriginaResponseBody=builder.build()
val retryRequest: Request
if (request.method() == "POST") {
retryRequest=request.newBuilder()
.url(request.url())
.post(retryOriginaResponseBody)
.build()
} else {
retryRequest=request.newBuilder()
.url(newRetryHttpUrl)
.build()
}
val retryResponse=chain.proceed(retryRequest)
return retryResponse
} else {
return tokenRefreshResponse
}
}
}
return response
}
}