I recently started learning how to develop mobile applications on Android. I've been doing mostly backend work before I joined RiseUp, but in the past 2 years I've learned to love web development, and mobile development seemed like a natural next step.
The first three weeks were awesome! Google created an amazing set of hands-on tutorials you can do to get started. But then I wanted to test my skills with real-world scenarios, and decided to implement our login flow in Android. The flow itself is not that interesting so I won't go into details there. What's important is that we use Nginx in our local (dev) environment to funnel all requests to our backend services. Nginx is responsible not only for routing API calls to the right service, but also for SSL termination. This allows our backend services to avoid meddling with SSL termination on their own.
Our local Nginx listens to port 9090, so I thought something like this should work pretty well*:
val loginApiService = Retrofit.Builder()
val token = loginApiService.authenticate(..)
*I simplified the code to make it more concise.
But when I ran the application, I got the following error:
java.net.ConnectException: Failed to connect to localhost/127.0.0.1:9090
Say what??? Well… apparently localhost on mobile devices is the device itself. That actually makes sense.
What I really wanted is that any calls to
localhost should go to my laptop and not the device.
adb to the rescue!
Android Debug Bridge (adb) is a CLI tool that helps you communicate with your mobile devices (virtual or physical). You can easily install in using brew:
brew install android-platform-tools
It has a cool command called
reverse that allows you to forward any calls from any device to your laptop. This is how it's done:
adb reverse tcp:9090 tcp:9090
This basically tells the device that every TCP call on port 9090 should be forwarded to port 9090 on your laptop.
Problem solved! So I ran my application again and this time I got this:
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
Our local Nginx uses a self-signed SSL certificate. It was created by an
openssl command. This worked great for web development and we didn't have any real issues with it so far.
With Android (and iOS) devices, you need to tell your device to trust that certificate. There are two steps here:
First you need to tell your device to trust the root CA of your self signed certificate. Unfortunately, when using
openssl to generate a certificate, you don't really have a root CA. So first I needed to change that.
I searched the Internet and found a great tool called mkcert.
mkcert is a simple tool for making locally-trusted development certificates (I shamelessly copied from their README. Sue me).
mkcert you also get a PEM encoded CA Certificate which is exactly what our Android device is expecting. To find that certificate you can run:
echo "$(mkcert -CAROOT)/rootCA.pem"
Copy the certificate to your device. Now on your device open the _CA Certificate _ settings screen and follow the instructions to install the certificate you copied.
By default, Android applications will not trust user added CAs. To make it trust your certificate you need to create a new file under
network_security_config with the following content:
<!-- Trust preinstalled CAs -->
<certificates src="system" />
<!-- Additionally trust user added CAs -->
<certificates src="user" />
Notice that we're trusting user added certificates only in debug mode. Released applications should work against properly signed SSL certificates.
Now add the configuration file to your AndroidManifest file:
That's it! 🎉
P.S. This works well for both physical and virtual devices.
To summarize, in order to enable your Android application to connect with an HTTPs server on your laptop with a self-signed certificate, you should:
adbto forward all calls to localhost
- Generate a root CA certificate and install in on your device
- Tell your application to trust user added CAs