How to add network_security_config.xml to manifest in expo app without ejecting

I was facing a similar problem (needed to connect to a local API with self-signed certificate) and after a ridiculous amount of research and experimentation I was finally able to find a solution. You will need to create a config plugin, which requires Expo SDK version 41+. Note that you will lose the ability to use Expo Go, but you will remain in the managed workflow (i.e. no need to change native code) and you can use EAS builds to build a custom dev client, which is basically a version of Expo Go tailored to your project.

Add the certificate to your device’s list of user certificates:

(This step is probably unnecessary if you include your (raw) certificate in the network config below, see this link.) Go to Settings -> Security -> Advanced -> Encryption & credentials -> Install a certificate to import the certificate

Ensure that the certificate is actually the problem:

try {
    const response = await axios.post(urlPath, payload);
} catch (error) {
    console.error(error.request?._response);
}

You’ll know your app doesn’t trust the certificate if you get a network error and error.request._response reads java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

Create the plugin:

You will now create a config plugin, which are basically JS functions which run during Expo’s prebuild phase to modify native configuration such as the Android manifest before building the native project.

In your project root, create a plugins folder with the following two files:

  • network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true">
        <trust-anchors>
            <certificates src="system" />
            <certificates src="user" />
        </trust-anchors>
    </base-config>
</network-security-config>
  • trust-local-certs.js:
const {AndroidConfig, withAndroidManifest } = require('@expo/config-plugins');
const {Paths} = require('@expo/config-plugins/build/android');
const path = require('path');
const fs = require('fs');
const fsPromises = fs.promises;

const { getMainApplicationOrThrow} = AndroidConfig.Manifest

const withTrustLocalCerts = config => {
    return withAndroidManifest(config, async config => {
        config.modResults = await setCustomConfigAsync(config, config.modResults);
        return config;
    });
}

async function setCustomConfigAsync(
    config,
    androidManifest
) {

    const src_file_pat = path.join(__dirname, "network_security_config.xml");
    const res_file_path = path.join(await Paths.getResourceFolderAsync(config.modRequest.projectRoot),
        "xml", "network_security_config.xml");

    const res_dir = path.resolve(res_file_path, "..");

    if (!fs.existsSync(res_dir)) {
        await fsPromises.mkdir(res_dir);
    }

    try {
        await fsPromises.copyFile(src_file_pat, res_file_path);
    } catch (e) {
        throw e;
    }

    const mainApplication = getMainApplicationOrThrow(androidManifest);
    mainApplication.$["android:networkSecurityConfig"] = "@xml/network_security_config";

    return androidManifest;
}

module.exports = withTrustLocalCerts;

Run expo prebuild & link the plugin

In order to use the plugin, you have to a have a file called app.json in the project root. I’m not 100% sure where I got the file from, but I believe it was created automatically when I first ran expo prebuild. Note:

  • I recommend upgrading the Expo SDK to the most current version (currently 44), because now the prebuild command (which apparently is the same as eject) is now completely reversible (e.g. it doesn’t install most of the additional dependencies it used to).
  • Prebuild will create the native folders, but you can safely delete these folders once you are done setting up the plugin (You really should delete them, if you want to remain in the managed workflow! When you run an EAS build, EAS will assume the bare workflow if it sees the native folders, and presumably not run your plugin again).
  1. Run expo prebuild and follow the prompts -> This should create the app.json file.
  2. Delete the generated native android folder (and ios, if it got created as well).
  3. In app.json, add the following to the end of the expo key:
    "plugins": [
      "./plugins/trust-local-certs.js"
    ]   
    
  4. Run expo prebuild --no-install and check that android/app/src/main/AndroidManifest.xml contains a reference to your network config, and that android/app/src/main/res/xml/network_security_config.xml was correctly copied over from your plugins directory.
  5. If all is well, your plugin is set up correctly and you should again delete the native folders (see note above).

Setup and run EAS build:

If you haven’t done so already, set up your project for EAS builds by following
these instructions
(install EAS CLI, run eas build:configure and configure a dev profile in eas.json). Then, run eas build --profile development --platform android. This will create a custom dev client in the cloud (running the plugin during the prebuild phase), which you can install on your device and acts as a replacement for Expo Go. To start the metro server, run expo start --dev-client. Your dev client should then be able to pick up the connection to the Metro server and if you run the axios request again, it should go through 🙂

Leave a Comment