Editor's note: This post was originally published in January 2018 and has been revamped and updated for accuracy and comprehensiveness. The latest update was in October 2022.
OAuth2, often combined with OpenID-Connect (OIDC), is a popular authorization framework that enables applications to protect resources from unauthorized access. It delegates user authentication to an authorization service, which then authorizes third-party applications to access the protected resources on the user’s behalf. OAuth2 provides authorization flows for both web and mobile applications.
In this article you will learn how to protect the OAuth2 Authorization Flow and have its client secret dynamically and securely delivered just-in-time to make the API request for the authorization code exchange with the access and ID token. This is achieved with the use of a Mobile App Attestation cloud service that only delivers the secret to genuine and unmodified versions of your mobile app, that are not under attack and running in a trusted environment, as defined by your security policies. On top of this the HTTPS channel will also be protected against MitM attacks by using the Managed Trust Roots features of the same cloud service, which can be updated on the fly without the need to release a new version of your mobile app.
The article introduction may overwhelm you with some new technical terms that you may not be very familiar with or haven't heard of yet so we’ll define them here.
This is the process of authenticating that a running instance of a mobile app is the exact same one that was uploaded to the app store. This process consists of attesting that the mobile app is not running on a compromised device, hasn’t been modified in any way, isn’t being manipulated during runtime, isn’t a target of an ongoing MitM attack, etc.
These are secrets provided just-in-time to the mobile app at runtime via secure over-the-air updates from a cloud service, at the moment they are required to make the API requests. Since they are protected by the Mobile App Attestation service on retrieval and subsequent usage in the API calls, the secrets are not delivered if the mobile app is under attack or running in an untrusted environment.
The traditional approach to protect the HTTP channel against MitM attacks is to secure it with certificate pinning, but this isn’t viable when the backend API you want to pin is not under your control. In such situations the public key pin can be changed at any time and without any notice, thus breaking your app until you release a new version with updated pins and all of your user base has upgraded to it.
To solve this limitation of traditional certificate pinning your app can have access to a set of trusted roots with the same set of certificates authorities typically trusted by the Android and iOS system trusted sites. These trusted roots can be securely updated in your code on the fly without the need to release a new version of your mobile app.
OAuth2's most popular flow is the authorization code grant flow which is used to authorize confidential clients’ access to protected resources. Clients use a client id to identify themselves and a client secret to authenticate themselves to the authorization service.
OAuth2 Authorization Code Grant Flow
Authorization code grant has good separation of frontend and backend flows. The frontend flow is delegated to a user agent, typically the system browser, which asks for the user credentials when not logged-in yet with the OAuth2 provider, and also asks the user to grant authorization permissions for the app to access protected resources. Upon success, an authorization code is returned to the app. In the backend flow, the app is authenticated by a client_secret in the requests for exchanging the authorization code for access and refresh tokens. The app then uses the access token to access protected backend resources on behalf of the user.
During the code grant flow, the client secret is only exposed to the authorization server. It is never exposed through the potentially less secure frontend user agent.
In OAuth2, a confidential client is one who can securely protect client secrets. Unfortunately, native apps are considered public, not confidential clients. They are not able to protect static secrets. Obfuscation and code hardening techniques make secrets harder to steal, but not impossible. If the client secret can be stolen, then it can be used by anyone to complete an authorization code exchange. This is one part of what makes mobile security so difficult.
Since a public client secret is no secret at all, many identity and authorization service providers simply drop the client secret. AppAuth, a popular open source Android and iOS OAuth2 SDK, recommends against utilizing client secrets:
Utilizing client secrets (DANGEROUS)
We strongly recommend you avoid using static client secrets in your native applications whenever possible. Client secrets derived via a dynamic client registration are safe to use, but static client secrets can be easily extracted from your apps and allow others to impersonate your app and steal user data. If client secrets must be used by the OAuth2 provider you are integrating with, we strongly recommend performing the code exchange step on your backend, where the client secret can be kept hidden.
Public clients are open to all kinds of attacks including authorization code and token theft and client impersonation by malicious software. With either an insecure secret or no secret at all, to restore the integrity of the OAuth2 code grant flow for mobile, native app protection must improve from public to confidential client strength.
On a public client using the basic code grant flow, anyone who can observe a frontend authorization code can attempt to exchange it for access and refresh tokens. Proof Key for Code Exchange (PKCE) was added to the basic flow to help offset this weakness. It attempts to ensure that the client initiating the frontend code request is the same client that subsequently requests the backend code exchange.
The client first generates a runtime secret called the code_verifier. In the stronger form of PKCE, the client hashes this secret and sends this code_challenge value as part of the frontend request. The authorization server saves this value. With or without a client secret, the client includes the code verifier as part of its subsequent backend code exchange request. The authorization server compares a hash of the code_verifier with the original code_challenge it received. If they match, the service will process the code exchange request as usual.
OAuth2 Authorization Code Grant Flow with PKCE
With PKCE, a malicious actor who steals the authorization code cannot successfully complete the code exchange without knowing the original code verifier. The code verifier is a runtime generated secret. As such, it is ephemeral and need not be persisted on the client, so the code verifier can be considered confidential on the mobile client.
A possible attack around PKCE would require generating a fake code_verifier and injecting the corresponding fake code_challenge hash into the client’s original frontend request. On observing the returned authorization code, the malicious actor can then send its fake code_verifier to complete the exchange.
Techniques such as SSL/TLS and certificate pinning can prevent these types of attacks, but they cannot eliminate them for good, because certificate pinning can be bypassed on a device that a malicious user or an attacker is in control of, as seen on the article How to Bypass Certificate Pinning with Frida on an Android App. Using this or other bypass techniques a malicious user or an attacker can login through the app to learn how it uses the backend API to then build a bot to scrape or attack the API to extract data from it or to perform actions that otherwise wouldn’t be possible via the app. Another attack surface to be aware of is when your mobile app is repackaged or cloned with the same OAuth2 credentials, thus being able to impersonate your mobile app and its authorization flow, allowing this way the backend API to be accessed as if the request was from the original app.
To secure the OAuth2 flow we will be using the OAuth2 web flow which requires a client_secret instead of the PKCE challenge to protect the authorization code exchange with access and refresh tokens. With a simple extension to the code grant flow, you can replace a static client_secret with the Approov Runtime Secret feature to securely deliver the client_secret just-in-time of being used.
Since the mobile app now receives the client_secret just-in-time to use on the authorization code exchange for the access and refresh tokens, and since the client_secret is only delivered when the app passes Mobile App Attestation, the app can now be considered a confidential OAuth2 client, matching the requirements for a secure authorization code grant flow.
Most importantly, since any tampering with the mobile app will cause attestation to fail, fake apps can no longer impersonate a valid client during the OAuth2 flow.
To follow along, start by cloning the Books demo project on GitHub available at github.com/approov. It requires some configuration that we will go through in the next steps, so it will not run out of the box.
The Approov mobile app attestation cloud service is used to establish trust between the Books App and the Google authentication server and Google books API. To continue following along you need to register for a free trial (no credit card required) and install the Approov CLI to access the Approov cloud service for registration of runtime secrets and mobile app APKs.
The commands you are about to execute require admin privileges, therefore you will need to switch from your developer role to an admin role.
First, open a terminal to be able use your Approov CLI, and start by enabling the admin role:
eval `approov role admin`
Now that your Approov CLI is ready you can start by enabling the managed trust roots.
When making API requests you need to protect the secrets within them from being extracted. Certificate pinning is normally the solution for this, but for third-party APIs you cannot pin against their certificates because you are not in control of when they will be rotated.
To solve this, let’s enable managed trust roots for the Approov service and integrate it into the Books App:
approov pin -setManagedTrustRoots on
This is necessary to block the use of self-signed certificates which are commonly used during a MitM attack, because now any request going through the Approov service will only be able to use official certificates, i.e. only those issued by Certificate Authorities.
Next, we need to inform the Approov service about the API domain(s) we want to protect. These are the destination domains of the API requests being made out of your mobile app. For the Books App they are the oauth2.googleapis.com, accounts.google.com, and www.googleapis.com:
approov api -add oauth2.googleapis.com -noApproovToken
approov api -add accounts.google.com -noApproovToken
approov api -add www.googleapis.com -noApproovToken
This completes the configuration for protecting the API calls from MitM attacks.
If you want to be able to see the Approov Service rejections in the logs of Android Studio, then you need to enable the policy to show the reason for the exception.
approov policy -setRejectionReasons on
These secrets are provided to the mobile app at runtime via secure over-the-air updates from the Approov cloud service, just-in-time as required to make API requests. They are protected with Mobile App Attestation on retrieval and subsequent usage in the API calls.
When the mobile app starts, an initial mobile app attestation is immediately launched via the SDK provided by Approov. This SDK will call the attestation service, resulting in some challenges for the app to execute and, depending on the results received, the attestation service will decide if it is trustworthy or untrustworthy. Once it is established that the mobile is not running on a compromised device (rooted, jail-broken, etc.), hasn’t been tampered with (re-signed, cloned, modified at runtime, etc.) and is not actively under attack (MitM attack, Instrumentation Framework, etc), then it can receive the runtime secret, because the mobile app has attested successfully. This is the secure delivery method to provide the runtime secrets.
To use the Approov Runtime Secrets Protection you need to enable the Approov secure strings feature:
approov secstrings -setEnabled
With this feature enabled you can remove all the secrets present in your mobile app and get them just-in-time when they are needed at runtime to make the API requests.
You will be using the Google’s Books API to perform open and authorized searches on Android. This requires a OAuth2 authorization screen in order to get the user consent for the Books App to have access to the private portions of the API, such as finding your favorite books.
First go to the Google developer’s console and sign in. Select or create a new project.
Next, click in Credentials on the left menu and then on + Create Credentials in the center of the nav bar. Now select the type OAuth client ID to create the OAuth2 credentials to use in your project:
Now, you might expect to use the Android application type, but it will not generate a client secret. Instead, make sure you select the Web application type:
Next, you should be able to fill the form to create your OAuth2 web application credentials:
The important part here is to enter an Authorized application URI that you will then set in your local.properties file to be then used in the mobile app to intercept the redirection from the Google OAuth2 screen. The URI doesn’t need to be available on your API backend; it just needs to have a valid format, e.g. https://example.com/oauth2redirect .
Now, copy and paste the Web Application OAuth2 Client Credentials into your local.properties file as per the /android/local.properties.example file:
google.authorizationScope=openid email profile https://www.googleapis.com/auth/books
Oh, but where is the placeholder for the Google client secret? Remember that we will be using the Approov runtime secrets, thus you need to add it with the Approov CLI:
approov secstrings -addKey client_secret -predefinedValue ___YOUR_GOOGLE_CLIENT_SECRET_HERE___
The gradle build will insert this configuration information into your application as it is building. Remember that your local.properties is ignored by git, so neither of these values will be tracked by Git.
Now, create the OAuth2 consent screen:
Finally, go to Google API Libraries page and find and enable the Google Books API.
In Adding OAuth2 to Mobile Android and iOS Clients Using the AppAuth SDK, the Android Books App example, which searches and finds favorite books, was developed demonstrating Google OAuth2, Google APIs, and Android AppAuth libraries. We will now use AppAuth with the OAuth2 web flow that requires a client_secret, that will be delivered just-in-time of being used by the Approov Runtime Secrets feature. This makes it possible to securely use the OAuth2 web flow in a mobile app, because the client secret is only delivered to mobile app instances that attest successfully with the Approov Mobile App Attestation cloud service.
You should now be able to successfully build the Books App. Once it has been built correctly, you can test it out in an Android emulator or real device, and you should be able to successfully search for books:
To test the OAuth2 service, select the login item in the book client pull down (the three vertical dots in the right upper corner). This should launch an OAuth2 consent screen:
Use any Google user credentials to login, and you should now see a login icon in the client’s top bar, and in the pull down menu, the favorite item should be enabled, therefore if you tap on it you should see your favorites:
For more usage details and debug, refer to the Adding OAuth2 to Mobile Android and iOS Clients Using the AppAuth SDK article.
First, you need to start by adding the Approov service dependency to the mobile app, and for that you need to open the file android/build.gradle and add the line to enable jitpack in order to require the dependency from the Git repository on Github:
Next, open the file android/app/build.gradle and add the lines to require the Approov services implementations:
In the case of the Books App we need to use two different Approov Services, because the app uses two different HTTP stacks, specifically HttpsURLConnection for the OAuth2 flow and OkHttpClient for the Google books API. On your own mobile app you may be using only one HTTP stack, therefore you would need to adjust as required. We support many different ways of adding Approov to your own iOS or Android app, and you can visit our Quickstart Integration page to see which ones we support, and if you don’t find what you need, please contact us to request support for your use case.
Now, copy/paste the Approov Config string from your onboarding email, or alternatively use the Approov CLI to get it:
approov sdk -getConfigString
Then, open the src/client/android/local.properties file and add to it the Approov config string (Note that it is not a secret):
Even though the Approov config string is not a secret you shouldn’t commit it into your source code. That is why we are adding it here.
Now, go back to the file android/app/build.gradle and uncomment the line that loads it into the default config as a resource value:
Next, hit that button on Android Studio to synchronize Gradle, and if there are no errors you are ready to start using the Approov services in the Books App.
To use Approov, open the android/app/src/main/java/com/criticalblue/auth/demo/BooksApp.java class and uncomment just three lines of code to import and initialize the Approov service. You also need to comment out the native OkHttpClient so that the one wrapped by Approov is used. When finished your BooksApp.java class should look like this:
The first Approov Service being initialized wraps the native HttpsURLConnection client and will be used to secure the OAuth2 web flow, provided via the AppAuth SDK, that uses the HttpsURLConnection client under the hood. The Approov service for the HttpsURLConnection client will securely deliver the required Oauth2 client_secret just-in-time to perform the authorization code exchange request, but only to mobile apps that pass the Mobile App Attestation challenges from the Approov cloud service. The Approov service also prevents the MitM attacks on the HTTPS communication channel by using the Approov Managed Trust Roots feature to validate the certificates negotiated on the HTTPS handshake, instead of using the device trust store, that can be manipulated as you can see being done in the article How to MitM Attack the API of an Android App.
The second Approov service being initialized wraps the native OkHttpClient and will be used to protect the API requests to the Google books API and to download the books images. The reason for using the OkHttpClient is that it is the one used, under the hood, by the Picasso library to download the book's images. In this context the Approov Service will be used to prevent MitM attacks on the API requests that could be performed to steal the Authorization bearer token from the API request headers, or to just tamper with the returned results or to replace the downloaded images. The Authorization bearer token was the one securely obtained in the OAuth2 authorization code exchange protected with the first Approov service for the HttpsURLConnection client.
The Approov Mobile App Attestation service is now ready to be used to protect outgoing requests from being tampered with and to provide runtime secrets just-in-time of being used, but only when the app passes the Mobile App Attestation.
The Approov service will be used for the AppAuth SDK HttpsURLConnection service to protect all outgoing requests against MitM attacks via the Approov Managed Trusted Roots feature to secure API requests to endpoints you are not in control of. When you control the API endpoint we recommend instead the use of Approov Dynamic Certificate Pinning feature. Both features support over-the-air secure updates to the trusted roots and pins respectively.
You will also switch from using the OAuth2 Mobile Flow to use the more robust and secure OAuth2 Web Flow that uses a runtime client_secret delivered securely by Approov just-in-time of being used on the authorization code exchange request.
Time to open the android/app/src/main/java/com/criticalblue/auth/demo/auth/AuthRepo.java class and to modify it for Approov by commenting and uncomment the lines as instructed in the file, after which it should look like this:
On the AuthRepo.performCodeExchangeRequest(), you have switched from using the OAuth2 mobile flow to use the more secure web flow, and this change also requires other changes in the AndroidManifest.xml file, where you need to comment and uncomment the lines as per the instructions in the file comments, leaving you with this:
Immediately after the AuthRepo.performCodeExchangeRequest() function, you can see how the client_secret is securely fetched just-in-time to be used for the authorization code exchange request in the AuthRepo.fetchJustInTimeClientSecret() function:
The AuthRepo.fetchJustInTimeClientSecret() function is then invoked from the AuthRepo.performWebFlowTokenRequest() function each time the user logs in and authorizes the Books App.
After a user is authenticated with the AppAuth SDK, they can access their favorites, requiring the Authorization bearer token obtained from the OAuth2 authorization code exchange request. This token is protected against a MitM attack by the Approov service for the HttpsURLConnection client used by AppAuth SDK.
API requests to the Google Books API will be handled by the BooksRepo that uses the Retrofit package that wraps the OkHttpClient; therefore we will use the Approov service for the OkHttpClient to protect these requests from being tampered with in order to extract the Authorization token. If you look into the BookRepo.createBooksAPI() function, you will see that an OkHttpClient is supplied to create a new Retrofit instance and retrieved from the BooksApp.getHttpClient() function that returns an OkHttpClient wrapped by Approov, preventing MitM attacks via the Approov Managed Trust Roots feature.
The books cover images don’t require any API key or Authorization token to be downloaded, but it’s still good security hygiene to protect their download, otherwise they can be replaced by other images. In a more sophisticated attack the replaced images could contain a payload in them to execute some commands which could download and inject malware on the device.
The images are downloaded in the BookListAdapter.onBindViewHolder() function by invoking BooksApp.loadImage() function which uses the Picasso image downloader from the BooksApp.getImageDownloader() function.
Click the run button in Android Studio to build, install and run the Books App in a real device, and you should get this screen:
Now, try to login and you should be able to progress through the OAuth2 consent screen and then be presented with this login failure:
If you look into the Android Studio logs you will see several entries like this:
This occurs because the Approov cloud service is not yet aware of the mobile app release you are using, thus the mobile app fails attestation and the client_secret is not provided to be used to exchange the OAuth2 authorization code for the access and refresh token; therefore the login is aborted with a message. To solve this, you need to register each release of the app with Approov, which you will do now.
From the root of this repo execute:
approov registration -add android/app/build/intermediates/apk/debug/app-debug.apk
For development, this process of registering the app each time you change the code is only necessary when testing that the Approov integration works; therefore you should whitelist your device to always pass the Mobile App Attestation:
approov device -add yourdeviceidbase64== -policy default,always-pass,all
Read Extracting the Device ID in the Approov docs to learn how to extract it from the logs or from the Approov token.
Before you restart the mobile app you need to wait around 30 seconds for the registration to propagate through the Approov cloud service infrastructure.
If, after restarting the mobile app, you still don’t reach the OAuth2 screen and the logs still show app-not-registered, then your Android Studio may be saving the APK in another location. Try registering with this command:
approov registration -add src/client/android/app/build/outputs/apk/debug/app-debug.apk
Now, you should be able to restart the mobile app and then be able to login.
If you rebuild the Books App from scratch and reinstall it, logins will fail as the newly installed app is different from the original one. Repeat the registration commands to start approving new logins from the new app.
This proof of concept successfully demonstrates adding the Approov Runtime Secrets protection to allow the use of the client secret with the more robust web authorization grant flow, instead of the less secure mobile grant flow which doesn’t use a client secret - it only relies on PKCE to secure the authorization code exchange with an access and ID token. It also shows how the Approov Managed Trust Roots feature is used to secure the authorization code grant flow by securing the HTTPS channel against MitM attacks.
The enhanced secured web authorization flow is compatible with the mobile native code grant flow, now requiring a client secret, which can be kept secure in the public client. This is achieved through use of the Approov Mobile App Attestation service with its runtime secrets protection and MitM attack prevention features.
Thanks for reading! For more information on mobile API security, check out www.approov.io.