Johannes Schneiders
Editor's note: This post was originally published in June 2018 and has been revamped and updated for accuracy and comprehensiveness. The latest update was in March 2021.
In this blog we use the integration of Approov with Cordova as an example for how to integrate Approov with a mobile app development platform while keeping changes to the platform and the apps that are built on top of it to a minimum. This is a quite detailed exposition on how to build an Approov plugin to use with Cordova Advanced HTTP. The integration techniques used here are also relevant for how one would use Approov on other mobile app development platforms. If you would just like to know how to use the Approov plugin, head straight to Using Approov in a Cordova App or try the Approov Cordova quickstart, but if you are curious about the details of how the integration works, read on.
Cordova is a platform for building native mobile applications using HTML, CSS and JavaScript. It is an open source project managed by the Apache Software Foundation.
Approov improves mobile security by enabling dynamic software attestation for mobile apps. It allows mobile apps to uniquely authenticate themselves as the genuine, untampered software you originally published. Upon successfully passing the integrity check the app is issued a short-lifetime token which can then be presented to your API with each request. This allows your server side implementation to differentiate between requests from known apps, which will contain a valid token, and requests from other sources, which will not. This gives you complete control over what you allow to talk to your mobile API server.
Cordova Advanced HTTP (also on NPM) is a popular Cordova plugin for communicating with HTTP(S) servers and works for both Android and iOS. Throughout this blog we are using Cordova Advanced HTTP as the example for Approov integration.
Before we look at the actual integration with a platform, we need to know a little more about how Approov works and how you use it with a native app. Approov is provided as a native SDK (Android .aar, iOS .framework and .xcframework) that you integrate into your Android and iOS apps (see also the Approov SDK User Guides for Android and iOS).
The Approov SDK provides a simple API for authenticating a mobile app and requesting an Approov token from the Approov Cloud Service to represent the app’s authenticity. The app can then transmit the token to your mobile API servers for validation:
The SDK initiates the token fetch to which the Approov Service replies with a challenge to the SDK to prove its own and the app's authenticity. The SDK responds to the challenge by performing an integrity check of itself and an authenticity check of the app, the result of which is verified remotely by the Approov Cloud Service. The Approov Cloud Service then provides the SDK with a unique, cryptographically-signed, time-limited Approov token whose validity is only known by the Approov Cloud Service and the API servers which hold the “token secret”.
While the Approov token representing the app’s authenticity is securely delivered to the SDK, and by extension the app, it is the mobile app's responsibility to ensure the token is securely transmitted to the mobile API server. The connection between app and API must use HTTPS and be pinned to the API server to prevent theft of valid Approov tokens through a man-in-the-middle (MitM) attack, because a stolen, valid Approov token can be used to impersonate a bona-fide app.
This can be achieved either by traditional static certificate pinning or through Approov's dynamic pinning capability.
Static pinning techniques specify the certificate or its public key that you are expecting from the API server and ensure that secure connections are only made to that server, thus blocking MITM attempts.
Static pinning can be difficult to manage operationally due to dependencies between app and server. The app needs to know the certificate to pin and this is usually embedded in the app. This means that a certificate update on the API server requires that the app must also be updated, which can be mitigated to some extent by having a second, backup certificate embedded in the app.
Ideally what is required is a means to transmit the updated pins over-the-air immediately to the app without any need for an app update. This needs to be done in a secure manner to prevent an attacker using this as a back door to inject their own pins to undermine the pinning protection.
Approov holds the set of public key pins for API domains being protected, inside an SDK configuration file. This initial configuration file is distributed as part of the app. The SDK configuration is signed using Elliptic Curve Cryptography (ECC), with the public key held in the initial SDK configuration and the private key held securely in Approov’s servers. If any change is made to the pins, then an updated dynamic configuration will be transmitted to any app that requests a new Approov token. The dynamic configuration is signed with the ECC private key, preventing any possibility of tampering and proving that the update has been issued by the Approov servers. This updated dynamic configuration will be written to the local storage of the app, and will either have an impact next time the app is started or immediately, depending upon the implementation of the pinning in the app. This verified updated configuration overrides the settings from the initial SDK configuration.
For a more detailed explanation and an example please refer to the Approov MITM Detection documentation.
The native Approov SDK provides a lean interface that consists of just a handful of functions.
initialize(application context, initial configuration, dynamic configuration, comment)
Before using any features of Approov you will first need to initialize the Approov SDK. Initialization should be performed at application start-up so the Approov SDK is available to perform attestations straight away. On Android, Approov initialization requires the application context. The initial configuration must be non-null but the dynamic configuration may be null. The comment parameter is reserved for future use and should be set to null. If there is a problem with the initialization then an exception will be thrown and it will not be possible to execute further methods in the SDK.
ApproovTokenFetchResult fetchApproovTokenAndWait(domain)
fetchApproovToken(ApproovTokenFetchCallback, domain)
Synchronously or asynchronously perform an attestation of the running app and fetch an Approov token from the Approov Service. The synchronous version blocks the caller until completion (or timeout) and returns the result of the token fetch operation. The asynchronous version returns immediately and on completion (or time out) invokes the callback with the result of the token fetch operation.setDataHashInToken(data)
Includes a hash of arbitrary data as one of the Approov token’s claims. This is intended for long lived data, such as an OAuth token or a user session id, that can be used to uniquely identify a user. This token binding can be used to extend the security of the Approov token through a strength in depth approach. getPins()
Returns the currently configured set of pins to be used, keyed by domain. The pins will be set up immediately after the initialization of the Approov SDK. Usage:
The Cordova Advanced HTTP plugin provides functions for communicating with HTTP(S) servers for both Android and iOS. Its Android implementation is based on Http Request a convenience library for using an HttpURLConnection to make requests and retrieve the response. Its iOS implementation uses AFNetworking a networking library built on top of Apple's Foundation framework's URL Loading System.
Cordova Advanced HTTP supports the corresponding HTTP(S) requests through the JavaScript functions post, get, put, patch, delete and head, and also provides functionality for uploading and downloading files through its uploadFile and downloadFilefunctions. All these functions take an URL, some data or query parameters, headers and success and error callbacks as their arguments and on completion, dependent on outcome, call the success or error function with the response as its argument.
Example: The function post(url, data, headers, success, failure) takes five arguments and calls the appropriate response function:
We want to provide a ready solution for using Approov with Cordova and are basing this on the Cordova Advanced HTTP plugin. But we also want to give a more general guide on how to integrate Approov into Cordova apps that use a different plugin for their HTTP(S) communication and provide a blueprint for how to integrate Approov with app development platforms in general.
Our overriding goals are that the details of using Approov should stay hidden as much as possible and that the effort for anyone to use Approov in their Cordova apps, be they existing or future ones, be kept to a minimum. We ended up with these:
This leads directly to our integration approach:
We are making no changes to the JavaScript interface of the existing HTTP(S) plugin that we are basing our integration on - that means there are no changes in the way existing apps use the HTTP(S) plugin, examples continue to work, etc. But we are extending the plugin's implementation by adding general-purpose interceptor hooks to the existing HTTP(S) plugin so additional functionality can be called if required. As long as these hooks are not used, the plugin's functionality and behaviour are not affected. Our aim is for these hooks to be general and useful enough that they will be accepted as contributions by the maintainers of the existing HTTP(S) plugin. Failing that, since we are only adding small amounts of code to the plugin, it should be easy to keep our version up to date with respect to the original.
In Cordova Advanced HTTP we add a hook just before the call that sends an HTTP(S) request. This hook can be used by anyone who wants to apply "last-minute" changes to a request.
We use these hooks to trigger the execution of interceptors that implement the necessary Approov functionality. Because the calls to the native Approov SDK are performed by the interceptors, an existing mobile app that uses the Cordova Advanced HTTP plugin does not require any code change with regards to its use of the plugin.
We provide the interceptor implementation as a separate Cordova plugin called Cordova Approov HTTP (using the naming style of Cordova Advanced HTTP). The plugin implements all necessary Approov functionality in native code, not JavaScript, effectively hiding the details of the Approov attestation scheme from the app. This is for ease of use and security, but also because access to request headers, connections, certificates, etc is usually not available at the JavaScript level.
The Cordova Approov Plugin provides all necessary functionality for using the Approov scheme (fetching an Approov token, adding it to a HTTPS request, setting up dynamic SSL pinning before each request, if so configured, handling a failure to establish a TLS connection) and performs all the necessary calls to the native Approov SDK.
The only change required to an app is the import of the Approov plugin. This makes for a pretty flat learning curve for anyone who is already using the Cordova Advanced HTTP plugin.
One final thing to be aware of is that requests that always succeeded before Approov protection with dynamic certificate pinning was enabled, may now fail because of a MITM attack or a genuine certificate update - which is of course the purpose of dynamic pinning. To recover from this when the attack ends or to start using a new genuine certificate, such requests must be re-tried - something many mobile apps will do for failed requests anyway, but if an app does not, this would be a further necessary change.
Looking forward, the approach outlined here is the blueprint for Approov integration with other platforms and plugins: Implement the hooks and re-use the Approov-specific code shown here.
On to the actual implementation:
Here is one we made earlier: the complete source code of Cordova Advanced HTTP with interceptor hooks is on GitHub.
On Android we add a hook just before the request operation which then allows us to install a custom hostname verifier that performs the certificate check and, if there is a pinning related problem (such as an MITM attack), prevents the request from going out and to clear the Approov SDK's certificate cache.
Cordova Advanced HTTP uses kevinsawicki@gmail.com's HttpRequest
In file cordova-plugin-advanced-http/src/android/com/synconset/cordovahttp/CordovaHttpBase.java:
Define an interface to be implemented by an interceptor. The interface's accept() method takes an HTTP request (that is ready to be sent) as its argument and can perform an action based on the contents of the request or modify the request.
// Interface type for request interceptors
public interface IHttpRequestInterceptor {
public void accept(HttpRequest request);
}
Add the shared (between all instances of CordovaHttp) list of request interceptors
// List of request interceptors
private static Deque<IHttpRequestInterceptor> requestInterceptors = new LinkedList<IHttpRequestInterceptor>();
Provide functions to add interceptors to the list and to apply all interceptors to an HTTP request. Interceptors can only be added to the front of the list to ensure that interceptors added later cannot prevent earlier added interceptors from running or cannot modify earlier changes made to the request.
// Add a request interceptor to the list of request interceptors
public static synchronized void addRequestInterceptor(IHttpRequestInterceptor requestInterceptor) {
if (requestInterceptor == null) {
throw new NullPointerException("Request interceptor must not be null");
}
CordovaHttp.requestInterceptors.addFirst(requestInterceptor);
}
Interceptors are applied in reverse insertion order, i.e. most recently added interceptor first.
// Apply all request interceptors
public static synchronized void applyRequestInterceptors(HttpRequest request) {
for (IHttpRequestInterceptor requestInterceptor : requestInterceptors) {
requestInterceptor.accept(request);
}
}
Call the interceptors just before sending the request. In function prepareRequest() add a call to applyRequestInterceptors() as the last action of the function.
protected void prepareRequest(HttpRequest request) throws HttpRequestException, JSONException {
this.setupRedirect(request);
this.setupSecurity(request);
request.readTimeout(this.getRequestTimeout());
request.acceptCharset(ACCEPTED_CHARSETS);
request.headers(this.getHeadersMap());
request.uncompress(true);
// Call interceptors to allow "last-minute" changes before performing the request
this.applyRequestInterceptors(request);
}
For iOS things are a little different because there one does not create a request directly, but creates a task (NSURLSessionDataTask) using a manager object that knows how to generate the request and callback functions for handling success or failure of the request operation. This means that we need to put our "request" hook just before the invocation of the manager's request generating function (GET, POST, etc).
Cordova Advanced HTTP uses the Alamo Fire Objective-C implementation.
In file cordova-plugin-advanced-http/src/ios/CordovaHttpPlugin.m:
// Type for request interceptor
typedef void (^RequestInterceptor)(AFHTTPSessionManager *manager, NSString *urlString);
// Lists of request interceptors
static NSMutableArray<RequestInterceptor>* requestInterceptors = nil;
// Add a request interceptor to the list of request interceptors
+ (void)addRequestInterceptor:(RequestInterceptor)requestInterceptor {
if (requestInterceptor != nil) {
@synchronized(requestInterceptors) {
[requestInterceptors insertObject:requestInterceptor atIndex:0];
}
}
}
// Apply all request interceptors
- (void)applyRequestInterceptorsToManager:(AFHTTPSessionManager*)manager URL:(NSString*)urlString {
@synchronized(requestInterceptors) {
for (RequestInterceptor requestInterceptor in requestInterceptors) {
requestInterceptor(manager, urlString);
}
}
}
- (void)executeRequestWithData:(CDVInvokedUrlCommand*)command withMethod:(NSString*)method {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
…
// Run in background as the request interceptors that are called below may be blocking
[self.commandDelegate runInBackground:^{
@try {
// Call interceptors to allow "last-minute" changes before performing the request
[self applyRequestInterceptorsToManager:manager URL:url];
…
if ([serializerName isEqualToString:@"multipart"]) {
[manager uploadTaskWithHTTPMethod:method URLString:url parameters:nil constructingBodyWithBlock:constructBody progress:nil success:onSuccess failure:onFailure];
} else {
[manager uploadTaskWithHTTPMethod:method URLString:url parameters:data progress:nil success:onSuccess failure:onFailure];
}
}
@catch (NSException *exception) {
[[SDNetworkActivityIndicator sharedActivityIndicator] stopActivity];
[self handleException:exception withCommand:command];
}
}];
}
The interface of Cordova Approov HTTP consists of just two functions that are related to token binding.
In file cordova-plugin-approov-http/www/approov-http.js:
approovSetDataHashInToken: function (data, success, failure)
provides access to the Approov SDK function of the same name.
approovSetBindingHeader: function (header, success, failure)
allows to specify a particular HTTP header that holds the source data for the call to approovSetDataHashInToken. For every request sent, the data is automatically extracted from the specified header.
As we have the necessary hooks for Approov to be called from Cordova Advanced HTTP, we can have a look at what code we need to execute for each Approov protected HTTPS request.
Recap: In order to use Approov with Approov token theft protection, we need to implement several bits:
On Android we are using a custom hostname verifier for the pinning check. This hostname verifier is called during the process of establishing a secure (TLS) connection to check that the certificate hash of the host to which the connection is attempted, matches the desired pin and, if not (failure: SSL connection failed), causes the TLS handshake to be aborted.
CordovaApproovHttpPlugin
CordovaApproovHttpPlugin performs Approov initialization, token fetching, adding the token to a request header, setting up certificate pinning, and certificate checking. We are only showing the parts of the code that are directly relevant to understanding this functionality.
setupApproovCertPinning() creates a custom hostname verifier that performs checking of the remote host’s certificate hash against the pins managed by the Approov SDK. Then it sets this certificate pinner on the connection to ensure that it is called when creating the connection.
// Set up Approov certificate pinning
public static void setupApproovCertPinning(HttpRequest request) throws HttpRequestException {
// Set the hostname verifier on the connection (must be HTTPS)
final HttpURLConnection connection = request.getConnection();
if (!(connection instanceof HttpsURLConnection))
{
IOException e = new IOException("Approov protected connection must be HTTPS");
throw new HttpRequestException(e);
}
final HttpsURLConnection httpsConnection = ((HttpsURLConnection) connection);
HostnameVerifier currentVerifier = httpsConnection.getHostnameVerifier();
if (currentVerifier instanceof CordovaApproovHttpPinningVerifier)
{
IOException e = new IOException("There can only be one Approov certificate pinner for a connection");
throw new HttpRequestException(e);
}
// Create a hostname verifier that uses Approov's dynamic pinning approach and set it on the connection
CordovaApproovHttpPinningVerifier verifier = new CordovaApproovHttpPinningVerifier(currentVerifier);
httpsConnection.setHostnameVerifier(verifier);
}
The interceptor is using the hook in Cordova Advanced HTTP. The hook is called immediately before the HTTPS request is sent. The interceptor first handles any binding header if set, fetches a token and, if successful, adds the received token to the header of the HTTPS request. Otherwise it handles any failure cases. Finally it sets up pinning for the request.
// Consumer (operates via side-effects) that sets up Approov protection for a request
public static CordovaHttpPlugin.IHttpRequestInterceptor approovProtect =
new CordovaHttpPlugin.IHttpRequestInterceptor() {
@Override
public void accept(HttpRequest request) {
// update the data hash based on any token binding header
if (bindingHeader != null) {
String headerValue = request.getConnection().getRequestProperty(bindingHeader);
if (headerValue == null)
throw new RuntimeException("Approov missing token binding header: " + bindingHeader);
Approov.setDataHashInToken(headerValue);
}
// request an Approov token for the domain
URL url = request.url();
String host = url.getHost();
Approov.TokenFetchResult approovResults = Approov.fetchApproovTokenAndWait(host);
// provide information about the obtained token or error (note "approov token -check" can
// be used to check the validity of the token and if you use token annotations they
// will appear here to determine why a request is being rejected)
Log.i(TAG, "Approov Token for " + host + ": " + approovResults.getLoggableToken());
// update any dynamic configuration
if (approovResults.isConfigChanged()) {
// Save the updated Approov configuration
saveApproovConfigUpdate();
}
// check the status of the Approov token fetch
switch (approovResults.getStatus()) {
case SUCCESS:
// Token was successfully received - add Approov header containing the token to the request
request.header(APPROOV_HEADER, APPROOV_TOKEN_PREFIX + approovResults.getToken());
break;
case UNKNOWN_URL:
// provided URL is not one that is configured for Approov
break;
case UNPROTECTED_URL:
// provided URL does not need an Approov token
break;
case NO_APPROOV_SERVICE:
// no token could be obtained, perhaps because Approov services are down
break;
default:
// A fail here means that the SDK could not get an Approov token. Throw an the
// exception containingstate error
throw new RuntimeException("Approov token fetch failed: " + approovResults.getStatus().toString());
}
// ensure the connection is pinned
setupApproovCertPinning(request);
}
};
CordovaApproovPinningVerifier
CordovaApproovPinningVerifier is the custom hostname verifier that apart from performing the hostname check, also checks the certificate pinning.
public final class CordovaApproovHttpPinningVerifier implements HostnameVerifier {
/** The HostnameVerifier you would normally be using. */
private final HostnameVerifier delegate;
/** Tag for log messages */
private static final String TAG = "CordovaApproovHttpPinningVerifier";
/**
* Construct a CordovaApproovHttpPinningVerifier which delegates the initial verify to a user
* defined HostnameVerifier before applying public key pinning on top.
*
* @param delegate the HostnameVerifier to apply before the custom pinning
*/
public CordovaApproovHttpPinningVerifier(HostnameVerifier delegate) {
this.delegate = delegate;
}
The verify() method is called during establishing an SSL connection. Our custom hostname verifier overrides this method, but before performing its pinning check, calls the original verify() method of the superclass. It then calls our certificate pinner with the leaf certificate obtained from the SSL session.
@Override
public boolean verify(String hostname, SSLSession session) {
// check the delegate function first and only proceed if it passes
if (delegate == null || delegate.verify(hostname, session)) try {
// extract the set of valid pins for the hostname
Set<String> hostPins = new HashSet<>();
Map<String, List<String>> pins = Approov.getPins("public-key-sha256");
for (Map.Entry<String, List<String>> entry: pins.entrySet()) {
if (entry.getKey().equals(hostname)) {
for (String pin: entry.getValue())
hostPins.add(pin);
}
}
// if there are no pins then we accept any certificate / public key
if (hostPins.isEmpty())
return true;
// check to see if any of the pins are in the certificate chain
for (Certificate cert: session.getPeerCertificates()) {
if (cert instanceof X509Certificate) {
X509Certificate x509Cert = (X509Certificate)cert;
ByteString digest = ByteString.of(x509Cert.getPublicKey().getEncoded()).sha256();
String hash = digest.base64();
if (hostPins.contains(hash))
return true;
}
else
Log.e(TAG, "Certificate not X.509");
}
// the connection is rejected
return false;
} catch (SSLException e) {
throw new RuntimeException(e);
}
return false;
}
The full iOS source code of Cordova Approov HTTP is available on GitHub.
As for Android, we are only showing the bits that deal with token fetching, adding token to request, setting up pinning and checking certificates.
In file cordova-plugin-approov-http/src/ios/CordovaApproovHttpPlugin.m, method pluginInitialize:
The request interceptor uses the request hook in Cordova Advanced HTTP which is called immediately before the manager's HTTPS request method is invoked. The interceptor first handles any binding header if set. Then fetches a token and, if successful, adds the received token to the header of the HTTPS request. Otherwise it handles any failure cases. Finally it sets up pinning for the request.approovProtect = ^(AFHTTPSessionManager *manager, NSString *urlString) {
// update the data hash based on any token binding header
if (![bindingHeader isEqualToString:@""]) {
@synchronized(bindingHeader) {
NSString *headerValue = [manager.requestSerializer valueForHTTPHeaderField: bindingHeader];
if (headerValue == nil) {
NSException* approovMissingBindingHeaderException =
[NSException exceptionWithName:@"ApproovMissingBindingHeaderException"
reason:@"Request is missing the Approov binding header"
userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"%@%@",
@"Request is missing the Approov binding header: ", bindingHeader]}];
@throw approovMissingBindingHeaderException;
}
[Approov setDataHashInToken:headerValue];
}
}
// Fetch the Approov token
ApproovTokenFetchResult *approovResult = [Approov fetchApproovTokenAndWait:urlString];
// provide information about the obtained token or error (note "approov token -check" can
// be used to check the validity of the token and if you use token annotations they
// will appear here to determine why a request is being rejected)
NSURL *url = [NSURL URLWithString:urlString];
NSLog(@"%@: Approov Token for %@: %@", TAG, [url host], [approovResult loggableToken]);
// update any dynamic configuration
if ([approovResult isConfigChanged]) {
// Save the updated Approov configuration
[CordovaApproovHttpPlugin saveApproovConfigUpdate];
}
NSString *approovToken = [approovResult token];
ApproovTokenFetchStatus approovStatus = [approovResult status];
switch (approovStatus) {
case ApproovTokenFetchStatusSuccess:
{
// Token was successfully received
// Add Approov header containing the token
[manager.requestSerializer setValue:[NSString stringWithFormat:@"%@%@", APPROOV_TOKEN_PREFIX, approovToken]
forHTTPHeaderField:APPROOV_HEADER];
break;
}
case ApproovTokenFetchStatusUnknownURL:
// Provided URL is a for a domain that has not been set up in the Approov Service
break;
case ApproovTokenFetchStatusUnprotectedURL:
// Provided URL does not need an Approov token
break;
case ApproovTokenFetchStatusNoApproovService:
// No token could be obtained, perhaps because Approov services are down
break;
default:
{
// A fail here means that the SDK could not get an Approov token. Throw an exception containing the
// state error
NSException* approovTokenFetchFailedException =
[NSException exceptionWithName:@"ApproovTokenFetchFailedException"
reason:@"Approov could not fetch an Approov token. The unprotected HTTP request must not proceed"
userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"%@%@%@",
@"Approov could not fetch an Approov token. Status: ",
[Approov stringFromApproovTokenFetchStatus:approovStatus],
@". The unprotected HTTP request must not proceed"]}];
@throw approovTokenFetchFailedException;
// Alternatively invalidate the session
// [manager invalidateSessionCancelingTasks:YES];
break;
}}
[CordovaApproovHttpPlugin setupApproovPublicKeyPinning:manager];
};
Phew, that is a lot of stuff. But it's all for the purpose of making the integration into an actual app simple. It will also make the integration of Approov or some other service with an app development platform easier next time, as we can re-use the ideas and the code that we have written.
Now that the integration of Approov mobile API protection with the Cordova platform is complete and available for download, what is left is to do the easy bit. We can now use Cordova Advanced HTTP can in the same way as before, but with Approov protection in place - enabling the API server to reject requests that do not originate from a bona-fide app.
Example call to Cordova Advanced HTTP's get request function (note: no changes from what we would normally do):
cordova.plugin.http.get("https://my.domain.com/endpoint", {}, {},
function(response) {
// Success
if (response.status == 200) {
console.log("Successfully performed GET request");
}
},
function(response) {
if (response.status != 200) {
// Failure
console.log("Error on GET request: " + response.status);
}
});
When building the mobile app it is important to ensure that the modified Cordova Advanced HTTP with the interceptor hooks is picked up by Cordova, not the original Cordova Advanced HTTP plugin. You also need to include the Cordova Approov HTTP plugin from GitHub in your Cordova project. Then build the mobile app as normal.
You can now run the mobile app, but it will not authenticate until you have registered it with the Approov Cloud Service.
Once the mobile app is registered and after a short propagation delay of no more than 30s, the mobile app will be recognized as valid by our service and will be issued tokens that your mobile API server can check for validity in order to reject bogus traffic.
Note: Attaching a debugger or using a rooted device will be detected by the Approov SDK and you will not get a valid token.
That's it, thanks for listening.