OkHttp Interceptors with Retrofit

·

14 min read

OkHttp Interceptors with Retrofit

Image for post

source: Square

Retrofit is a popular, simple and flexible library to handle network requests in android development. The ability for it to be adapted to different use cases is quite amazing as you can inject custom OkHttpClient Interceptors to intercept, change or modify requests and responses, call adapter factory for supporting service method return types other than the usual Call as well as converter factory for serialization and deserialization of object using Gson, Moshi, Jackson, etc.

In this post, I’m going to show with an example of how we can adapt OkHttpClient interceptors to encrypt and decrypt requests and responses to and from a server.

Use Case

Let’s assume that as part of the security routine or standards in your organization, requests, and responses to and from the server must be encrypted over the network. How then do you handle such using the Retrofit library?

*Interceptors to the Rescue


OkHttp library exposes an Interceptor interface which observes, modifies, and potentially short-circuits requests going out and the corresponding responses coming back in. Typically interceptors add, remove, or transform headers on the request or response.

In this example, we would create an Encryption and Decryption class that implements the Interceptor interface and override the intercept method. It is in the overridden method that we’ll handle our encryption and decryption before passing the class as part of the OkHttpClient builder. Easy innit?

Let’s see this in code…

We’ll use a utility class that handles the encryption and decryption and also use Postman mock server to mock our request and response.

Note

The encryption/decryption mechanism used here is just to depict the whole process and should not be used for a production app. The necessary security documents of your firm should be consulted.

As stated, our mock Api would only accept an encrypted string as a request as well as return an encrypted string as a response to the client.

Encryption Interceptor

Let's add our Encryption interceptor that implements the Interceptor interface as follows:

 public class EncryptionInterceptor implements Interceptor {
    private static final String TAG = EncryptionInterceptor.class.getSimpleName();
    private final CryptoStrategy mEncryptionStrategy;

     //injects the type of encryption to be used
    public EncryptionInterceptor(CryptoStrategy mEncryptionStrategy) {
        this.mEncryptionStrategy = mEncryptionStrategy;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Timber.i("===============ENCRYPTING REQUEST===============");
        Request request = chain.request();
        RequestBody rawBody = request.body();
        String encryptedBody = "";


        MediaType mediaType = MediaType.parse("text/plain; charset=utf-8");
        if (mEncryptionStrategy != null) {
            try {
                String rawBodyString = CryptoUtil.requestBodyToString(rawBody);
                encryptedBody = mEncryptionStrategy.encrypt(rawBodyStr);
                Timber.i("Raw body=> %s", rawBodyStr);
                Timber.i("Encrypted BODY=> %s", encryptedBody);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            throw new IllegalArgumentException("No encryption strategy!");
        }
        RequestBody body = RequestBody.create(mediaType, encryptedBody);

        //build new request
        request = request.
                newBuilder()
                .header("Content-Type", body.contentType().toString())
                .header("Content-Length", String.valueOf(body.contentLength()))
                .method(request.method(), body).build();

        return chain.proceed(request);
    }

}

Decryption Interceptor

Likewise, the response from the server needs to be decrypted and parsed to the necessary object. Let’s create a Decryption interceptor as follows:

  public class DecryptionInterceptor implements Interceptor {
     private final CryptoStrategy mDecryptionStrategy;

    //injects the type of decryption to be used
    public DecryptionInterceptor(CryptoStrategy mDecryptionStrategy) {
        this.mDecryptionStrategy = mDecryptionStrategy;
    }

    @Override
    public Response intercept(@NonNull Chain chain) throws IOException {
        Timber.i("===============DECRYPTING RESPONSE===============");

        Response response = chain.proceed(chain.request());
        if (response.isSuccessful()) {
            Response.Builder newResponse = response.newBuilder();
            String contentType = response.header("Content-Type");
            if (TextUtils.isEmpty(contentType)) contentType = "application/json";
            String responseString = response.body().string();


            String decryptedString = null;
            if (mDecryptionStrategy != null) {
                try {
                    decryptedString = mDecryptionStrategy.decrypt(responseStr);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                Timber.i("Response string => %s", responseStr);
                Timber.i("Decrypted BODY=> %s", decryptedString);
            } else {
                throw new IllegalArgumentException("No decryption strategy!");
            }
            newResponse.body(ResponseBody.create(MediaType.parse(contentType), decryptedString));
            return newResponse.build();
        }
        return response;
    }
}

Retrofit Api Service

The Retrofit saveBook service saves a Book object to the server. But the Encryption class is meant to intercept and convert the Book object to an encrypted string.

  public interface ApiService {
    @POST("book")
    Call<Book> saveBook(@Body Book book);
}

OkHttp and Retrofit Client

With the interceptors created, we need to build the OkHttpClient and add the Encryption and Decryption interceptors. We need to be mindful of the order in which we add the interceptors. Ideally, a request is first made to the server which in turn responds with a response. So we need to add the Encryption interceptor before the Decryption interceptor since it works on response data.

Once the interceptors have been added to the OkHttpClient, we can now add the OkHttpClient to the Retrofit builder as can be seen below.

public class App extends Application {
      private static final String TAG = App.class.getSimpleName();
     private static final String BASE_URL = "https://66d252d7-61d1-44e0-be70- 
    1f77477ac86c.mock.pstmn.io";

    private static App INSTANCE;
    private ApiService apiService;

    public static App get() {
        return INSTANCE;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        INSTANCE = this;

        OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder();

        //Gson Builder
        GsonBuilder gsonBuilder = new GsonBuilder();
        Gson gson = gsonBuilder.create();
        Timber.plant(new Timber.DebugTree());

        // HttpLoggingInterceptor
        HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(message -> Timber.i(message));
        httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);

        /**
         * injection of interceptors to handle encryption and decryption
         */
        //Encryption Interceptor
        EncryptionInterceptor encryptionInterceptor = new EncryptionInterceptor(new EncryptionImpl());
        //Decryption Interceptor
        DecryptionInterceptor decryptionInterceptor = new DecryptionInterceptor(new DecryptionImpl());

        // OkHttpClient. Be conscious with the order
        OkHttpClient okHttpClient = new OkHttpClient()
                .newBuilder()
                //httpLogging interceptor for logging network requests
                .addInterceptor(httpLoggingInterceptor)
                //Encryption interceptor for encryption of request data
                .addInterceptor(encryptionInterceptor)
                // interceptor for decryption of request data
                .addInterceptor(decryptionInterceptor)
                .build();

        //Retrofit
        Retrofit retrofit = new Retrofit.Builder()
                .client(okHttpClient)
                .baseUrl(BASE_URL)
                // for serialization
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();

        //ApiService
        apiService = retrofit.create(ApiService.class);

    }
    public ApiService getBookService() {
        return apiService;
    }
}

With the client in place, we can call the saveBook service in the MainActivity.java file as follows:

    public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button saveButton = findViewById(R.id.saveBook);


        ApiService bookService = App.get().getBookService();
        Book book = new Book("There was a Country", "Chinua Achebe", "Memoir");


        saveButton.setOnClickListener(view ->
                bookService.saveBook(book).
                        enqueue(new Callback<Book>() {
                            @Override
                            public void onResponse(Call<Book> call, Response<Book> response) {
                                if (response.isSuccessful()) {
                                    Timber.i("Response from Server::%s", response.body().toString());
                                }
                            }

                            @Override
                            public void onFailure(Call<Book> call, Throwable t) {
                                //do something

                            }
                        }));

    }
}

When you run the app, you’d find the something similar to the logs below:

I/App: --> POST https://66d252d7-61d1-44e0-be70-1f77477ac86c.mock.pstmn.io/book
    Content-Type: application/json; charset=UTF-8
I/App: Content-Length: 72
    {"author":"Chinua Achebe","genre":"Memoir","name":"There was a Country"}
    --> END POST (72-byte body)
I/EncryptionInterceptor: ===============ENCRYPTING REQUEST===============
I/EncryptionInterceptor: Raw body=> {"author":"Chinua Achebe","genre":"Memoir","name":"There was a Country"}
    Encrypted BODY=> 0MUa6xs4fjMT8VewdsjZLEPF7k/Sn3N6rYmT0kT2y1PYdvvqrspuKIQ7OBNOzIia/S4BAfJzxdp8
    nI2HZ/vYURWETS6jzREGMF8GyvkIW54=
I/DecryptionInterceptor: ===============DECRYPTING RESPONSE===============
I/DecryptionInterceptor: Response string => 0MUa6xs4fjMT8VewdsjZLEPF7k/Sn3N6rYmT0kT2y1PYdvvqrspuKIQ7OBNOzIia/S4BAfJzxdp8nI2HZ/vYURWETS6jzREGMF8GyvkIW54=
    Decrypted BODY=> {"author":"Chinua Achebe","genre":"Memoir","name":"There was a Country"}
I/App: <-- 200 OK https://66d252d7-61d1-44e0-be70-1f77477ac86c.mock.pstmn.io/book (388ms)
    Access-Control-Allow-Origin: *
    Content-Type: text/html; charset=utf-8
    Date: Sat, 19 Oct 2019 23:13:38 GMT
I/App: ETag: W/"6c-ToIkuX9ts4zcsZx/J7P8+/yPD+0"
    Server: nginx
    Vary: Accept-Encoding
    x-srv-span: v=1;s=d2bcd1957a018093
    x-srv-trace: v=1;t=49367fd029789a12
    Connection: keep-alive
    {"author":"Chinua Achebe","genre":"Memoir","name":"There was a Country"}
I/App: <-- END HTTP (72-byte body)
I/MainActivity: Decrypted Book ::Book{name='There was a Country', author='Chinua Achebe', genre='Memoir'}

Conclusion

From the use case explained, it can be seen that OkHttp Interceptors offer developers a great tool to manipulate requests and responses as well as transform content of request headers.

There are other use cases for OkHttp Interceptors, do well to check them out.


You can clone or fork the repository here and don’t forget to leave a comment.

Thanks.

Reference

OkHttp Interceptors