Security Architecture Completely Securing Mobile Application APIs
1. Overview
In this article, we are going to be looking at one of the main challenges of building a production-ready mobile application. Our ideal mobile application is one that is built using Google Mobile Services as it forms a core part of this architecture.
Furthermore, the type of mobile app that we’re looking at is one that’s backed by a backend service built with Java/PHP/Ruby/JavaScript etc. Hence, the mobile app needs to communicate with the backend service to deliver value to users.
This article is structured such that, we are going to start with a deep dive into the problem we’re trying to solve, present the solution and then touch on one or two gotchas before concluding.
2. The Problem
Let’s say we’re building a mobile app for an event booking platform. Users will sign up via the app by providing their phone number and password. Upon receiving the signup request, the backend will send an OTP to the user’s phone number for confirmation. Users can also reset their passwords, which also requires sending an OTP to the phone number.
This means the backend service will expose the following APIs to the mobile app, among others:
- /api/register
- /api/password/reset
- /api/resend_otp
These APIs are usually open because the user will not have any record or credential yet or the action happens before the user authenticates - which is the case for resetting a password. In my experience, malicious users can exploit these open endpoints by calling them with random data, in a script.
Going by our example event booking platform, where we send SMS to confirm the user’s phone number, this will cost the business money in SMS costs.
What about infrastructure costs and waste of man-hours? It will take effort to clean up the database if a malicious user exploits an unprotected registration API to create a lot of bad records. It can also create a false sense of progress where the business thinks new users are being onboarded.
Achieving security for these endpoints is the goal of this article and that’s what we will be exploring in the next section.
3. The Solution
In summary, when a user launches the app for the first time. The app will first authenticate itself and by extension the device, with Google Firebase by signing in anonymously on Firebase.
Next, the app will call an open endpoint on the backend with the Device ID and Firebase UUID. The backend will then generate a token and save it on Firebase’s Real-Time Database (RT Database) before responding with HTTP status 200 OK.
Finally, the app will proceed to securely read the token from RT Database using Firebase SDK and use the token for subsequent API calls. Especially, the hitherto open endpoints described above.
This way, we will only have one open endpoint, that does not require any form of authentication or API key and we will also be taking advantage of the already secured communications between Google Firebase and the mobile app.
The integration with Google Firebase was the missing piece. Let’s look at different architectural variations of this technique.
3.1 Using Static API Token
This variation is the easiest to implement and is not recommended for production use but useful for pedagogical purposes. The steps are as follows:
-
The engineer will generate a static API key and store it in Firebase RT Database using the Firebase console. Then the engineer will configure the RT Database rules to require that an app/device is at least anonymously authenticated. Anonymous authentication in Firebase simply means using a random username/password combination to create a user record
-
On the first startup of the mobile app, it will authenticate anonymously using the Firebase-auth library. This will grant the app’s instance the required level of permission to read data from the RT Database
-
The mobile app will then proceed to read the API key from the RT Database and supply it as part of every API call to the backend server
This approach is simple and may be suitable for apps that want relaxed authentication. The obvious shortcoming of this is that a malicious user can legally download the app, inspect the API calls and have access to the API key.
Because every app instance/user is using the same API key, then the endpoints are as good as the open ones. What if there’s a way we can make the generation of the API key to be dynamic and unique to a single app instance and user? Well, read on my friend.
3.2 Generating Dynamic API Token
This variation is more secure than the previous one in the sense that, every app instance will have its own unique API key associated with it.
-
During the first initialisation, the mobile app will authenticate anonymously to Google Firebase using the Firebase-auth library. This will grant the app’s instance the required level of permission to read data from the RT Database
-
Next, the app will send its deviceId and Firebase UUID to the backend in a bid to get an API key
-
The backend will generate an API key and save it against the Device ID and/or Firebase UUID before storing it in the RT Database. Then, the backend will simply respond with HTTP status 200 OK
-
The mobile app will then proceed to read the API key from the RT Database and add it to every other API call
While no solution is 100% secure, this is almost flawless. Every single app instance will have a different API key that they can use to securely communicate with the backend.
If a malicious user gets a hold of the app and inspects the network for the API key, the malicious user will not be able to get any unauthorised access to other people’s data.
Abusing the one open endpoint will only lead to the creation of redundant API keys in the backend’s database. We use a periodic job to simply clean up dormant API keys.
As good as this is, there’s yet another variant we can look at.
3.3 Dynamic API Key and Encryption Credentials
In my early days of researching this topic, this is the mode I came up with at first. It’s pretty complicated, but I’ll summarise it below:
-
Just like the first step above, on the first startup, the mobile app will perform anonymous authentication to Google Firebase to get the right permission to read data from the RT Database
-
The mobile app will send its Device ID and Firebase UUID to the backend to get a One-Time Password that will expire in X seconds
-
The backend will generate an OTP (A Time-based One-Time Password is better for this), associate it with the given Device Id and/or Firebase UUID and then store it in RT Database. After which it will respond with HTTP 200 OK
-
The mobile app will proceed to read the OTP from the RT Database and send it to the backend with the same Device Id and Firebase UUID as in step 1 for validation
-
If the backend is able to validate the given OTP, it will now generate a more permanent client credential, HMAC secret key and RSA key pair for the device. The backend will store the generated keys in the RT Database and respond with HTTP status 200 OK to the mobile app
-
The mobile app will again read the client credentials and RSA key pair from the RT Database. Furthermore, it will encrypt the credentials before storing them on the device
-
The mobile app now has the ability to generate an HMAC signature for subsequent requests and even encrypt the request data using the RSA public key. The backend service is now at liberty to expect client credentials and HMAC signature for APIs such as the reset password and signup
-
The backend service can also have a job that runs periodically to delete expired and used OTPs from its database and from Firebase RT Database. This will help reclaim resources that might be consumed as a result of an abusive user and keep infrastructure costs down
4. Potential Pitfalls
This architecture relies heavily on the secure connection between Google’s Firebase and the mobile app. This connection in turn relies heavily on how you as a company or an engineer are able to manage the Firebase credentials for the app.
Unauthorized access to your app’s Firebase credentials will allow a malicious user to access your Firebase resources and even create a clone of your app.
Talking of Firebase resources, it’s also pertinent that the Firebase Real-Time Database is secured with rules. Let’s consider the following rule:
|
|
The above rule grants write permissions to clients where the provider is backend-identity and also checks the auth property evaluates to be true - in short, only the backend can write to the RT Database.
The rule also stipulates that users/app instances can only read items written in the same path as their Firebase UUID and even at that, the user/app instance has to be authenticated.
5. Conclusion
If you read this article all the way to this point, kudos to you. It’s a long one, despite the fact that no code examples have been added there. So, I’ll be doing a follow-up on other aspects of this article, be sure to subscribe so as not to miss out.
I look forward to reading about your experience implementing this in your organisation in the comments below.
Further Readings
- Learn more about Firebase Anonymous Authentication on Android here
- Learn more about Reading from Firebase’s RT Database here
- Learn more about using the Firebase Admin SDK to write data to RT Database here
- Interested in learning about encrypting data in Android apps, you may consult this medium article
Happy Coding