Ktor - Only apply bearer tokens for certain urls - android

In my project, there are network requests that require authentication via bearer tokens and some requests that don't. Is there a way to specify the urls that do not need bearer tokens? If I just add the Auth plugin, then I always get a 401 response for the network calls that don't require bearer tokens.
This is my implementation right now:
interface MyService {
...
companion object Factory {
fun build(getToken: GetToken, login: Login, saveToken: SaveToken): MyService {
return MyServiceImpl(httpClient = HttpClient(CIO) {
install(JsonFeature) {
serializer = KotlinxSerializer(
kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
}
)
}
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.HEADERS
}
install(Auth) {
lateinit var tokenInfo: TokenInfo
bearer {
refreshTokens {
getRefreshedTokens(
tokenInfo = tokenInfo,
login = login,
saveToken = saveToken
)
}
loadTokens {
tokenInfo = getToken.execute().firstOrNull() ?: TokenInfo(
Constants.EMPTY_STRING, Constants.EMPTY_STRING
)
BearerTokens(
accessToken = tokenInfo.accessToken,
refreshToken = tokenInfo.refreshToken
)
}
}
}
})
}
...
}
}

You can filter requests by some condition that should include the Authorization header initially. For each other request, the second request with the Authorization header will be made if a server replied with 401 Unauthorized status. Here is an example configuration:
val client = HttpClient {
install(Auth) {
bearer {
sendWithoutRequest { request -> request.url.encodedPath.startsWith("/restricted") }
// ...
}
}
}

A bearer token is nothing but a header Authorization: Bearer XXX - If you're looking for a tech specific way to handle this (drop in some module, etc) then you may be out-of-luck
However, this problem occurs frequently enough in my life - the solution is to actually dynamically manipulate the headers - Basically, use the strategy pattern. If you can create instances of your client, then each instance would get its own strategy. If you can't, then install a function that gets called with every request to determine the header based on whatever logic you need.
In your case, a custom:
install(Auth) {
// your code here
}

Related

Firebase getIdToken(true) times out frequently

We are moving our backend to Firebase tokens, so I tried updating the Android app with the following code. We use Kotlin/Retrofit/OkHttp to make request. In order to add tokens correctly to all HTTP requests to the server I've created an interceptor in the following way:
class FirebaseAuthInterceptor: Interceptor {
private var token: String = ""
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val user = FirebaseAuth.getInstance().currentUser
if (user != null) {
runBlocking {
val tkn = getBearerToken(user)
token = "Bearer $tkn"
}
}
request = request.newBuilder().addHeader("authorization", token.trim()).build()
return chain.proceed(request)
}
#OptIn(ExperimentalCoroutinesApi::class)
private suspend fun getBearerToken(user: FirebaseUser): String =
withTimeout(TIMEOUT_MS) {
suspendCancellableCoroutine { emitter ->
user.getIdToken(true)//true
.addOnSuccessListener {
emitter.resume(it.token.toString()) {}
}
.addOnCanceledListener {
emitter.cancel(IllegalStateException("getIdToken canceled"))
Timber.e("requestResult: getIdToken cancelled")
}
.addOnFailureListener {
emitter.cancel(UnknownHostException())
Timber.e("requestResult: getIdToken failed")
}
}
}
}
However, it looks like withTimeout expires A LOT (according to the logs in our Firebase Crashlytics: 35 crash events affecting 22 users in the last 12h). I see the following error:
Fatal Exception: ck.x2: Timed out waiting for 60000 ms
at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:184)
at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:154)
Searching through the web, there really isn't any recommended way of going about this. What is the best way of passing the bearer token to HTTP requests, and always having a fresh one available on the device when the calls are made.
Thank you

How do I create a refresh token outside of Auth (bearer, basic, digest)?

I'm having trouble creating a refresh token in Kotlin Multiplatform, because the token I'm using is out of basic, bearer, and digest. Example code :
defaultRequest {
url {
protocol = URLProtocol.HTTPS
// Base URL
host = urlHost
headers {
append(HttpHeaders.ContentType, "application/json")
if (preferences.token.isNotEmpty()) {
append("token", "static")
} else {
append("token", preferences.token)
}
}
}
}
How do I update the token if an error code 401 occurs?

How to regenerate token in Android GraphQL?

What is the better approach to regenerate the JWT token using refresh token in Apollo Graphql in Andriod Kotlin?.
Now we are using an Auth interceptor that extends ApolloInterceptor to attach the JWT token in the request header.
I want to check whether the Toke expires from here and it should call another mutation to generate a new token. Then it should proceed with the previous call with the new token.
Please refer to the code below
class AuthInterceptor(private val jwtToken: String) : ApolloInterceptor {
override fun interceptAsync(
request: ApolloInterceptor.InterceptorRequest,
chain: ApolloInterceptorChain,
dispatcher: Executor,
callBack: ApolloInterceptor.CallBack
) {
val header = request.requestHeaders.toBuilder().addHeader(
HEADER_AUTHORIZATION,
"$HEADER_AUTHORIZATION_BEARER $jwtToken"
).build()
chain.proceedAsync(request.toBuilder().requestHeaders(header).build(), dispatcher, callBack)
}
override fun dispose() {}
companion object {
private const val HEADER_AUTHORIZATION = "Authorization"
private const val HEADER_AUTHORIZATION_BEARER = "Bearer"
}
}
If you are using Apollo Android Client V3 and Using Kotlin Coroutine then use Apollo Runtime Dependency and Try HttpInterceptor instead of ApolloInterceptor. I think this is the better/best approach. For Reference Click Here
In your app-level build.gradle file
plugins {
......
id("com.apollographql.apollo3").version("3.5.0")
.....
}
dependencies {
implementation("com.apollographql.apollo3:apollo-runtime")
}
Now write your interceptor for the Apollo client.
FYI: If you've added the Authorization header using Interceptor or using addHttpHeader in client already then remove it or don't add header here val response = chain.proceed(request.newBuilder().addHeader("Authorization", "Bearer $token").build()), just build the request. Otherwise Authorization header will add multiple times in the request. So, be careful.
class AuthorizationInterceptor #Inject constructor(
val tokenRepo: YourTokenRepo
) : HttpInterceptor {
private val mutex = Mutex()
override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain): HttpResponse {
var token = mutex.withLock {
// get current token
}
val response = chain.proceed(request.newBuilder().addHeader("Authorization", "Bearer $token").build())
return if (response.statusCode == 401) {
token = mutex.withLock {
// get new token from your refresh token API
}
chain.proceed(request.newBuilder().addHeader("Authorization", "Bearer $token").build())
} else {
response
}
}
}
Configure your Apollo client like below.
ApolloClient.Builder()
.httpServerUrl(BASE_GRAPHQL_URL)
.webSocketServerUrl(BASE_GRAPHQL_WEBSOCKET_ENDPOINT) // if needed
.addHttpHeader("Accept", "application/json")
.addHttpHeader("Content-Type", "application/json")
.addHttpHeader("User-Agent", userAgent)
.addHttpInterceptor(AuthorizationInterceptor(YourTokenRepo))
.httpExposeErrorBody(true)
.build()

How to differentiate between different Retrofit responses in Authenticators or Interceptors?

I have a basic Retrofit setup for network requests. I have the following Authenticator that is added to the chain. It basically tries to refresh access token when authorization error (401) occurs.
class TokenAuthenticator(private val api: MyApi) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
val retrofitResponse = api.refreshAccessToken("my refresh token here"))
val refreshResponse= retrofitResponse.blockingGet()
return if(refreshResponse != null) {
response.request().newBuilder()
.header(Const.HEADER_AUTHORIZATION, "Bearer " + refreshResponse.accessToken)
.build()
} else {
return null
}
}
}
The problem is my server might return 401 not only for authroization issues but also for some other cases. For example, i might get response with 401 if user phone number already exists in the database. Server returns me error_code paramter to differentiate this type of issues:
error_code = "token_expired" -> authoriation issue. shows that access token expired.
error_code = "phone_exists" -> shows phone number entered already exsits in the database.
So, I need to be able to check for this paramter before deciding that error was access token refresh error. How can I do that?
Currently, since I have not been able to check for that paramter, my app thinks that 401 is an authroization issue and continuously trying to refresh the access token even though my access token is not expired.
we had similar issue in our project, it may be a little mess because of hard-coded url but i think it is okey
we check the request's url and if it matched with the refresh token url then we start process of getting new token
Do you mean ErrorInterceptor?
import okhttp3.Interceptor
import okhttp3.Response
class ErrorInterceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain?): Response {
val originalResponse = chain!!.proceed(chain.request())
if (shouldLogout(originalResponse)) {
// your logout logic here
// send empty response down the chain
return Response.Builder().build()
}
return originalResponse
}
private fun shouldLogout(response: Response) : Boolean {
if (response.isSuccessful) {
return false
}
// 401 and auth token means that we need to logout
return (response.code() == 401 &&
!response.headers().names().contains(AUTH_HEADER_KEY))
}
}

Okhttp Authenticator multithreading

I am using OkHttp in my android application with several async requests. All requests require a token to be sent with the header. Sometimes I need to refresh the token using a RefreshToken, so I decided to use OkHttp's Authenticator class.
What will happen when 2 or more async requests get a 401 response code from the server at the same time? Would the Authenticator's authenticate() method be called for each request, or it will only called once for the first request that got a 401?
#Override
public Request authenticate(Proxy proxy, Response response) throws IOException
{
return null;
}
How to refresh token only once?
Use a singleton Authenticator
Make sure the method you use to manipulate the token is Synchronized
Count the number of retries to prevent excessive numbers of refresh
token calls
Make sure the API calls to get a fresh token and the
local storage transactions to save the new token in your local stores are not asynchronous. Or if you want to make them asynchronous make sure you to you token related stuff after they are completed.
Check if the access token is refreshed by another thread already to
avoid requesting a new access token from back-end
Here is a sample in Kotlin
#SingleTon
class TokenAuthenticator #Inject constructor(
private val tokenRepository: TokenRepository
) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
return if (isRequestRequiresAuth(response)) {
val request = response.request()
authenticateRequestUsingFreshAccessToken(request, retryCount(request) + 1)
} else {
null
}
}
private fun retryCount(request: Request): Int =
request.header("RetryCount")?.toInt() ?: 0
#Synchronized
private fun authenticateRequestUsingFreshAccessToken(
request: Request,
retryCount: Int
): Request? {
if (retryCount > 2) return null
tokenRepository.getAccessToken()?.let { lastSavedAccessToken ->
val accessTokenOfRequest = request.header("Authorization") // Some string manipulation needed here to get the token if you have a Bearer token
if (accessTokenOfRequest != lastSavedAccessToken) {
return getNewRequest(request, retryCount, lastSavedAccessToken)
}
}
tokenRepository.getFreshAccessToken()?.let { freshAccessToken ->
return getNewRequest(request, retryCount, freshAccessToken)
}
return null
}
private fun getNewRequest(request: Request, retryCount: Int, accessToken: String): Request {
return request.newBuilder()
.header("Authorization", "Bearer " + accessToken)
.header("RetryCount", "$retryCount")
.build()
}
private fun isRequestRequiresAuth(response: Response): Boolean {
val header = response.request().header("Authorization")
return header != null && header.startsWith("Bearer ")
}
}
I see here two scenarios based on how API which you call works.
First one is definitely easier to handle - calling new credentials (e.g. access token) doesn't expire old one. To achieve it you can add an extra flag to your credentials to say that credentials are being refreshed. When you got 401 response, you set flag to true, make a request to get new credentials and you save them only if flag equals true so only first response will be handled and rest of them will be ignored. Make sure that your access to flag is synchronized.
Another scenario is a little bit more tricky - every time when you call new credentials old one are set to be expired by server side. To handle it you I would introduce new object to be used as a semafore - it would be blocked every time when 'credentials are being refreshed'. To make sure that you'll make only one 'refresh credentials' call, you need to call it in block of code which is synchronized with flag. It can look like it:
synchronized(stateObject) {
if(!stateObject.isBeingRefreshed) return;
Response response = client.execute(request);
apiClient.setCredentials(response.getNewCredentials());
stateObject.isBeingRefreshed = false;
}
As you've noticed there is an extra check if(!stateObject.isBeingRefreshed) return; to cancel requesting new credentials by following requests which received 401 response.
In my case I implemented the Authenticator using the Singleton pattern. You can made synchronized that method authenticate. In his implementation, I check if the token from the request (getting the Request object from Response object received in the params of authenticate method) is the same that the saved in the device (I save the token in a SharedPreferences object).
If the token is the same, that means that it has not been refresed yet, so I execute the token refresh and the current request again.
If the token is not the same, that means that it has been refreshed before, so I execute the request again but using the token saved in the device.
If you need more help, please tell me and I will put some code here.
This is my solution to make sure to refresh token only once in a multi-threading case, using okhttp3.Authenticator:
class Reauthenticator : Authenticator {
override fun authenticate(route: Route?, response: Response?): Request? {
if (response == null) return null
val originalRequest = response.request()
if (originalRequest.header("Authorization") != null) return null // Already failed to authenticate
if (!isTokenValid()) { // Check if token is saved locally
synchronized(this) {
if (!isTokenValid()) { // Double check if another thread already saved a token locally
val jwt = retrieveToken() // HTTP call to get token
saveToken(jwt)
}
}
}
return originalRequest.newBuilder()
.header("Authorization", getToken())
.build()
}
}
You can even write a unit test for this case, too! 🎉
Add synchronized to authenticate() method signature.
And make sure getToken() method is blocking.
#Nullable
#Override
public synchronized Request authenticate(Route route, Response response) {
String newAccessToken = getToken();
return response.request().newBuilder()
.header("Authorization", "Bearer " + newAccessToken)
.build();
}
Make sure to use singleton custom Authenticator
When refreshing token successful return request with new token else return null.
class TokenAuthenticator(
private val sharedPref: SharedPref,
private val tokenRefreshApi: TokenRefreshApi
) : Authenticator,
SafeApiCall {
override fun authenticate(route: Route?, response: Response): Request? {
return runBlocking {
when (val tokenResponse = getUpdatedToken()) {
is Resource.Success -> {
val token = tokenResponse.data.token
sharedPref.saveToken(token)
response.request.newBuilder().header("Authorization", "Bearer $token").build()
}
else -> {
null
}
}
}
}
private suspend fun getUpdatedToken(): Resource<LoginResponse> {
return safeApiCall { tokenRefreshApi.refreshToken("Bearer ${sharedPref.getToken()}") }
}
}

Categories

Resources