Networking in Depth: OkHttp, JWT, OAuth, Pagination

Android Development with Kotlin: 21 Real Projects · Production Android · 5 min read · Updated

Token refresh

  1. An interceptor adds the access token to each request.
  2. The server rejects an expired token with status 401.
  3. The authenticator asks for a new token using the refresh token.
  4. The original request is repeated once with the new token.

Project 6 made one simple request. Real apps send authenticated requests, handle failures, upload files, page through results and keep data safe in transit. This lesson covers the pieces behind production networking.

HTTP and REST in short

  • GET reads, POST creates, PUT replaces, PATCH changes part, DELETE removes.
  • Status codes: 2xx success, 3xx redirect, 401 not signed in, 403 not allowed, 404 not found, 429 too many requests, 5xx server error.
  • Always use HTTPS. Android blocks plain HTTP by default, which is the right default.
  • JSON is the usual format. Kotlin serialization (or Gson/Moshi in older apps) converts it to classes. XML is rare and seen in older APIs.
interface ShopApi {
    @GET("products") suspend fun products(@Query("page") page: Int): List<ProductDto>
    @GET("products/{id}") suspend fun product(@Path("id") id: Int): ProductDto
    @POST("orders") suspend fun order(@Body body: OrderRequest): OrderDto
    @PATCH("profile") suspend fun update(@Body body: ProfilePatch): ProfileDto
    @DELETE("orders/{id}") suspend fun cancel(@Path("id") id: Int)
    @Multipart @POST("upload")
    suspend fun upload(@Part file: MultipartBody.Part): UploadDto
}

OkHttp and interceptors

Retrofit sits on OkHttp, which does the real work. An interceptor sees every request and response, so it is the right place to add a token, log traffic in debug builds, or add a language header.

class AuthInterceptor(private val tokens: TokenStore) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request().newBuilder()
            .header("Authorization", "Bearer " + tokens.access())
            .build()
        return chain.proceed(request)
    }
}

val client = OkHttpClient.Builder()
    .addInterceptor(AuthInterceptor(tokenStore))
    .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC })
    .connectTimeout(15, TimeUnit.SECONDS)
    .build()

Only log request bodies in debug builds. Logging tokens or passwords in a release app is a security fault.

Authentication: JWT and OAuth 2.0

After a user logs in with email and password, the server returns a JWT (JSON Web Token): a signed string that proves who the user is. It is short-lived (minutes), and usually comes with a longer-lived refresh token. Send the access token in the Authorization: Bearer header. When the server answers 401, use the refresh token to get a new access token and retry once. In OkHttp this is an Authenticator.

class TokenAuthenticator(private val tokens: TokenStore, private val auth: AuthApi) : Authenticator {
    override fun authenticate(route: Route?, response: Response): Request? {
        if (response.request.header("Retry") != null) return null     // give up after one retry
        val fresh = runBlocking { auth.refresh(tokens.refresh()) }.accessToken ?: return null
        tokens.save(access = fresh)
        return response.request.newBuilder()
            .header("Authorization", "Bearer " + fresh)
            .header("Retry", "1").build()
    }
}

OAuth 2.0 lets a user sign in through another provider (Google, GitHub) without giving your app their password. Mobile apps use the *authorization code flow with PKCE*, opened in a browser tab or with the Credential Manager for Google sign-in, and never embed a client secret. Use a maintained library (AppAuth, Credential Manager) rather than writing the flow yourself.

Errors, connectivity and results

Wrap calls so the UI receives a result, not an exception: catch IOException (no network), HttpException (a bad status) and parse the server's error body. Check connectivity with ConnectivityManager and registerDefaultNetworkCallback, but never rely on it alone: a call can still fail, so always handle errors. Retry only safe (idempotent) requests, with a growing delay.

Pagination, upload and download, caching

  • Pagination: page numbers (?page=2) or cursors (?after=abc). Use Paging 3 to drive it.
  • Upload: MultipartBody.Part with a RequestBody made from the file. Show progress by wrapping the body. For large or background uploads use WorkManager.
  • Download: stream a ResponseBody to a file in chunks, never load it all in memory.
  • Caching: OkHttp can cache responses that allow it with Cache(directory, size); Coil caches images; Room caches data you need offline.

Network security

  • Use HTTPS everywhere and do not disable certificate checks, even in tests.
  • Certificate pinning (CertificatePinner) makes the app trust only your server's key. It adds protection against some attacks, but if the certificate changes and you did not plan for it, the app stops working, so pin with backups.
  • Keep secrets on a server. Do not trust the app to hide an API key.
  • Use a network security config only if you must allow special rules.

GraphQL

GraphQL lets the client ask for exactly the fields it needs in one request. On Android the usual library is Apollo Kotlin, which generates Kotlin types from your queries. Choose it when your backend already offers GraphQL. REST with Retrofit remains the most common.

Your task

  1. Add an interceptor to a project that adds a header and logs the URL in debug builds only.
  2. Write a fake server response for a 401 using MockWebServer and test that the authenticator retries once.
  3. Upload an image with a multipart request to a test endpoint.
  4. Add a retry with growing delay for a GET request.

How to check it worked

  • Tokens never appear in release logs.
  • A 401 triggers one refresh and one retry, not a loop.
  • You can explain JWT, refresh token and OAuth in your own words.

Frequently asked questions

What is a JWT?

A signed token the server issues after login. The server can verify it without a database lookup. Treat it like a password: keep it in secure storage and never log it.

Is certificate pinning required?

No. It adds protection but also risk if a certificate changes. Many apps rely on normal HTTPS. High-risk apps such as banking often pin.

Where do I keep an API secret?

On your server. A key inside an app can be extracted, so the app should call your server and the server holds the secret.