Skip to main content

Overview

TecMeli implements OAuth 2.0 authentication for the Mercado Libre API using a robust token management system. The architecture ensures that every API request is automatically authenticated and tokens are seamlessly refreshed when they expire.

Architecture Components

The authentication system consists of three main components:

AuthInterceptor

Injects access tokens into outgoing requests

TokenAuthenticator

Handles automatic token refresh on 401 responses

TokenRepository

Manages token storage and refresh logic

How It Works

1. AuthInterceptor

The AuthInterceptor automatically adds the Authorization header to all API requests:
class AuthInterceptor @Inject constructor(
    private val tokenRepository: TokenRepository
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        
        // Skip token for OAuth endpoints
        if (originalRequest.url.encodedPath.contains("oauth/token")) {
            return chain.proceed(originalRequest)
        }

        val token = tokenRepository.getAccessToken()
        val requestBuilder = originalRequest.newBuilder()
        
        token?.let {
            requestBuilder.addHeader("Authorization", "Bearer $it")
        }
        
        return chain.proceed(requestBuilder.build())
    }
}
The interceptor smartly skips adding tokens to the /oauth/token endpoint to avoid circular dependencies during token refresh.

2. TokenAuthenticator

When the API returns a 401 (Unauthorized) response, the TokenAuthenticator automatically refreshes the token:
class TokenAuthenticator @Inject constructor(
    private val tokenRepository: TokenRepository
) : Authenticator {

    override fun authenticate(route: Route?, response: Response): Request? {
        if (response.code != 401) return null

        val result = runBlocking {
            tokenRepository.refreshToken()
        }

        return if (result.isSuccess) {
            val newToken = result.getOrNull()
            response.request.newBuilder()
                .header("Authorization", "Bearer $newToken")
                .build()
        } else {
            null
        }
    }
}
The authenticator uses runBlocking to handle the suspension function synchronously, as required by OkHttp’s Authenticator interface. This is an intentional design decision.

3. TokenRepository

The TokenRepository manages token storage and communicates with the Auth API:
@Singleton
class TokenRepositoryImpl @Inject constructor(
    private val authApi: AuthApi,
    private val apiConfig: ApiConfig
) : TokenRepository {

    private var accessToken: String? = null

    override fun getAccessToken(): String? {
        if (accessToken == null) {
            runBlocking {
                refreshToken()
            }
        }
        return accessToken
    }

    override suspend fun refreshToken(): Result<String> = safeApiCall(
        call = {
            authApi.refreshToken(
                clientId = apiConfig.clientId,
                clientSecret = apiConfig.clientSecret,
                refreshToken = apiConfig.refreshToken
            )
        },
        transform = { body ->
            accessToken = body.accessToken
            body.accessToken
        }
    )
}
See the implementation at:
  • core/network/TokenAuthenticator.kt:18
  • core/network/AuthInterceptor.kt:15
  • data/repository/TokenRepositoryImpl.kt:22

OAuth 2.0 Flow

1

Initial Configuration

The app is configured with clientId, clientSecret, and an initial refreshToken obtained during user login.
2

First API Request

When making the first API call, TokenRepository.getAccessToken() is called. Since no token exists, it automatically triggers a token refresh.
3

Token Injection

AuthInterceptor adds the access token to the Authorization header of the request.
4

Token Expiration

When a token expires, the API returns a 401 status code.
5

Automatic Refresh

TokenAuthenticator intercepts the 401 response and calls refreshToken() to obtain a new access token.
6

Retry Request

The original request is automatically retried with the new token.

API Configuration

The ApiConfig class centralizes all OAuth credentials:
data class ApiConfig(
    val clientId: String,
    val clientSecret: String,
    val refreshToken: String
)
These credentials are typically:
  • Stored securely (e.g., in encrypted SharedPreferences)
  • Injected via Hilt/Dagger dependency injection
  • Obtained from the Mercado Libre Developer Portal

Auth API Endpoint

The AuthApi interface defines the token refresh endpoint:
interface AuthApi {
    @POST("oauth/token")
    @FormUrlEncoded
    suspend fun refreshToken(
        @Field("grant_type") grantType: String = "refresh_token",
        @Field("client_id") clientId: String,
        @Field("client_secret") clientSecret: String,
        @Field("refresh_token") refreshToken: String
    ): Response<AuthResponseDto>
}
See data/remote/api/AuthApi.kt:14

Best Practices

The TokenRepositoryImpl is marked as @Singleton to ensure a single source of truth for the access token across the entire application. This prevents race conditions during concurrent requests.
Token refresh failures are handled gracefully by returning null from the authenticator, which signals OkHttp to fail the request and propagate the error to the caller.
Never log or expose tokens in production builds. Use ProGuard/R8 to obfuscate token-related code and store credentials in secure storage.

Next Steps

Network Layer

Learn about safe API calls and error handling

Error Handling

Understand how errors are mapped and handled

Build docs developers (and LLMs) love