OkHttp Interceptors with Retrofit
OkHttp Interceptors with Retrofit
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.