Expo + IoT: Device provisioning with HTTPS via WiFi
Development•React Native••16 minutes read
Vincent de Lafontaine
Guest Author
Learn how to connect IoT devices to Wi-Fi with a smooth provisioning flow using React Native, Expo, and react-native-wifi-reborn.

This is a guest post from Vincent de Lafontaine - a full-stack engineer and CTO of App & Flow. For almost 10 years, he has been leading the product team at App&Flow, consistently tackling and overcoming every tech challenge in his path.
...
TL;DR - The following article aims to show one of the ways you could setup an Expo project in a context where you want to build a companion app for an IoT device. This article will focus on the basic required setup needed to achieve provisioning in a companion app and is based originally on an IoT mandate we had at App and Flow.
I’m a total n00b when it comes to electronics. While I now feel pretty comfortable sitting at the very top of the OSI’s layers, at the beginning I knew almost nothing about the underlying layers and as the mighty philosopher Dwight K. Schrute used to say: knowledge is power. I needed to know more. But where to start?
In order to familiarize myself with something a bit more low level, I decided to take a look at IoT devices. At App and Flow, we had a couple of clients in the past where we needed to build companion apps for IoT devices. We were not in charge of anything related to building the device itself (as we specialize in mobile apps) but that part piqued my curiosity nonetheless. I eventually took up the challenge of building an IoT device myself and a companion app to go along with it.
Since then, I built way too many hydroponic “smart” gardens with a bunch of useless features (All my plants eventually died - I might be the problem though, remember I specialize in mobile apps not plant care), an app to control all my home IoT devices (yes basically my own google home. Don’t ask.) and finally an 18 volts battery car that I can drive in real time using an app I built and with a camera to get the cockpit view of the car…
I might make a series of articles to cover the whole process but today we’ll start with the companion app side and we’ll mostly focus on the provisioning flow which is essentially the very first step. Most of the IoT device use protocols such as MQTT for communications with the device using a pub/sub pattern. This process of communication generally starts once the provisioning is done. You’ll see how it’s now ridiculously easy to get a working prototype using Expo (thanks to the plugin system) and JuanSeBestia’s library **react-native-wifi-reborn** that we’ll use for the provisioning flow.
If you own multiple IoT devices, you probably noticed that the provisioning setup of such device is not always the same. Sometimes the setup is done by WiFi, sometimes it’s done using Bluetooth. In my personal opinion, the WiFi approach gives a better UX and that’s the one we’ll cover today.
It starts by having a way of sharing your WiFi credentials with the IoT device so that it can connect to your own network. Most of the time, a companion app is provided with an IoT device and it guides the user through the setup process. From that app you can generally select which network the device should connect to and start the provisioning flow.
Provisioning flow overview
Here’s a VERY minimalist overview featuring all the main actors and that we’ll use for the pairing flow:
At a high level, the pairing steps are quite simple. The whole idea is to get the IoT device connected to our WiFi, but how does it get our WiFi’s credentials?
Step 1: Detect and connect our phone to the IoT device’s WiFi network.
Step 2: Once on the device’s network, send our data (our main WiFi’s credentials) to the IoT device using a good old HTTP web server that lives on the device. We can hit the web server using the device IP provided by the device network. Generally these devices will have a reserved IP address so that the IP of the device can be known in advance.
Step 3: The device receives the WiFi credentials and tries to connect to the main WiFi network. Generally, once achieved, the device network ceases to exist and our phone gets automatically reconnected to our own main WiFi network.
Implementing the pairing flow
Initial setup
The following code is from a freshly initialized project on Expo SDK 52.
Step 1
Install and setup JuanSeBestia/react-native-wifi-reborn:
- npx expo install react-native-wifi-rebornStep 2
Add the following configuration to your app.json file:
{"expo": {"ios": {"infoPlist": {"NSLocalNetworkUsageDescription": "The app requires access to the local network so it can..."},"entitlements": {"com.apple.developer.networking.wifi-info": true}},"plugins": ["react-native-wifi-reborn", {"fineLocationPermission": true}]}}
Back in the day, we would have needed to eject from the managed flow in order to install such a library. Nowadays with Expo’s plugin system, it’s really easy and convenient to inject libraries that require native modules. To function, the desired library must have implemented expo plugin support which react-native-wifi-reborn did.
For Android, setting fineLocationPermission to true will automatically add android.permission.ACCESS_FINE_LOCATION to your android manifest. This permission is required for the WiFi module.
On Android, you’ll also need to ask for the fine location permission at run time. You can do so with this code.
Step 3
If you already have your bundle ID and your app set up on App Store Connect, retrieve your bundle ID, click on it and add the following capabilities (Access Wi-Fi Information and Hotspot):
Step 4
Now we’ll simply add a function to connect to a specific WiFi SSID when a button is pressed.
import React from 'react';import { Button, View, PermissionsAndroid } from 'react-native';import WifiManager from 'react-native-wifi-reborn';const WelcomeScreen = () => {const connectToSpecificSSID = async () => {try {if (Platform.OS === 'android') {const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,{title: 'Location permission is required for WiFi connections',message:'This app needs location permission as this is required ' +'to scan for wifi networks.',buttonNegative: 'DENY',buttonPositive: 'ALLOW',},);}const ssid = 'my-network';const password = 'my-network-password';const isWep = false; // what century is this?const isHidden = false;await WifiManager.connectToProtectedSSID(ssid, password, isWep, isHidden);} catch (e) {console.warn(e);}};return (<Viewstyle={{flex: 1,justifyContent: 'center',}}><Button title="Connect" onPress={connectToSpecificSSID} /></View>);};export default WelcomeScreen;
Naturally, do not commit your SSID credentials and you should not even have it hardcoded in your build if you are testing with your own WiFi network. Add some text inputs and enter the credentials yourself manually if needed.
Unfortunately for us, on iOS it is not possible to retrieve the list of available SSIDs within an app. That’s why we must know in advance the SSID we want to connect to. In our case, the SSID would be provided with our IoT device.
Now if you try this code on Expo Go directly on your simulator, you’ll be greeted by this error message in your console: [TypeError: Cannot read property 'connectToProtectedSSID' of null] and that’s totally expected. We need a development build. The generic Expo Go app does not have the required native code to directly use the library (because Expo Go is meant to be a sandbox).
You could also just create a native build and ship it to TestFlight but this won’t be convenient at all for active development. However, if you really don’t wish to go through the setup of making a dev build, know that you can skip the following section.
Step 5
Build a development build.
We’ll need to install expo-dev-client into our project to have a development build. Also, we won’t be able to use this library on a simulator. We’ll need to use our phone from that point to test the pairing flow. The easiest path for testing and developing at this point is to create an internal build using the following eas.json config.
First, run the following:
- npx expo install expo-dev-clientAt the root of your project, if you don’t already have an eas.json file, run this command eas build:configure and add this profile development in the “build section”.
{..."build": {"development": { "developmentClient": true, "distribution": "internal" },...},}
You’ll also need to register your device with your provisioning profile. You can follow Expo's documentation to achieve this.
Run eas device:create and follow the documentation above to register your own phone.
Step 6
Once you have your own development build installed on your phone, try to hit the connect button and see if your current network change to the targeted network. As mentioned above, I added two text inputs in order to type in my own network credentials and not have it hardcoded.
You should see a native dialog asking you if you wish to connect to another network. Once accepted, you can validate it by checking your current network in your settings. If everything went well, you should now be authenticated to the targeted network.
Pardon my french
If you get a behavior similar to this, it means that the library is working as expected.
Example of a simple provisioning flow close to reality (Soft AP Mode Provisioning)
Great so now that we know that the library is working as it should and is properly configured, we can actually start to work on implementing a provisioning flow between the device and our main WiFi network. The approach might be different depending on the device, but the global ideal should be to have a way of sharing our wifi credentials with the device. Normally, the device maker should provide all the specifications and the requirements you’ll need.
Security disclaimer
Keep in mind that most of the heavy work here regarding security is in the hardware maker’s court. Ideally the device would implement a standard such as DPP or use a cloud-based solution like AWS IoT for instance and make sure that everything is done in a secure way (or at least the most secure way). In the companion app side of things, the task is essentially to connect to another WiFi network and make a couple of HTTP calls.
If DPP is not used in a production context, at the very minimum, the web server would have a self signed SSL certificate to allow us to use HTTPS when exchanging our WiFi credentials with the device. That certificate would then be bundled with the companion app using SSL pinning or added to the phone profile. Otherwise someone could in theory connect to the device network while you are pairing it and do some packet sniffing which would expose your unencrypted WiFi password if plain HTTP is used.
An alternative would be to make sure that every device has it’s own unique WiFi password so that only someone physically owning the device would have the IoT network password.
If you don’t have the luxury of having one of these mechanisms in place, consider using an asymmetric key encryption pattern. Have a way of retrieving a public key (from a QR code or from another server for instance) to encrypt your data and a private key on the device to decrypt the incoming data. This is not ideal as the private key must be “hardcoded” in the device software and someone very invested could reverse-engineer the device and potentially get the private key..
Long story short, you cannot do a lot for security from a companion app perspective. The responsibility mostly falls into the device maker realm and each approach has it’s pros and cons. Often, the complexity of the device will reflect the targeted market. Hence why an IoT security camera designed for large corporations won’t have the same security acceptance as a smart garden for home usage.
For this article, we’ll suppose that the device provisioning is done using a unique WiFi password (each IoT device has a unique password) and that the web server running on the device is using HTTPS.
For the WiFi provisioning flow, you should have the following provided by the device maker
- The SSID you’ll be targeting with its unique password.
- The fixed ip address (or domain name if using Multicast DNS) hosting the web server you’ll send data to. Note that an IP would be better here as it adds a layer of protection against DNS spoofing.
- The API spec to communicate with the web server (like an open API document or any kind of documentation to know which endpoints to hit and what to send/retrieve)
- The root local CA certificate used to generate SSL certificates used by the device’s web server (if using HTTPS). You’ll need to manually install this profile on your phone. Once it’s downloaded and installed on your phone make sure to activate it: Settings → General → About → Certificate Trust Settings and switch the toggle on. If you don’t install the certificate on your phone, you’ll have some SSL error and the network call will fail.
We won’t go too much into the details of creating a self signed certificate but you can use this tool here: FiloSottile/mkcert and follow this tutorial for this section.
Editing the Android Manifest (only if you are not using HTTPS)
If you decide however not to use HTTPS, on Android, you’ll need to add the IP of the IoT device in your Android Manifest in order to allow the communication between our app and the device.
One way you could achieve this, without using expo prebuild and tweaking the manifest yourself directly, is to create your own small config plugin. We would then append this config into our app.json file.
If it fits your needs, you can use the following code to do so:
- At the root of your project create a
pluginsfolder and add these two files:
const { withAndroidManifest, AndroidConfig } = 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 withCustomNetworkSecurityConfig = (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 = withCustomNetworkSecurityConfig;
<?xml version="1.0" encoding="utf-8"?><network-security-config><domain-config cleartextTrafficPermitted="true"><domain includeSubdomains="true"><YOUR IOT DEVICE IP HERE></domain></domain-config></network-security-config>
(The fixed IP of the device before the provisioning)
Finally, add the following in the plugins section of your app.json:
"plugins": ["./plugins/add-security-config-plugin.js"]
App code
We’ll use this very simple code and UI to achieve our provisioning flow:
And here’s the code:
import React from 'react';import {ActivityIndicator,Alert,Button,Text,TextInput,View,Platform,PermissionsAndroid,} from 'react-native';import WifiManager from 'react-native-wifi-reborn';import * as Location from 'expo-location';async function delay(ms: number) {return new Promise((resolve) => setTimeout(resolve, ms));}const WelcomeScreen = () => {const [ssid, setSSID] = React.useState('');const [password, setPassword] = React.useState('');const [ssidMain, setSSIDMain] = React.useState('');const [passwordMain, setPasswordMain] = React.useState('');const [currentWifiSSID, setCurrentWifiSSID] = React.useState('');const [loading, setLoading] = React.useState(false);React.useEffect(() => {const getInitialSSID = async () => {try {await delay(1000);if (Platform.OS === 'android') {const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,{title: 'Location permission is required for WiFi connections',message:'This app needs location permission as this is required ' +'to scan for wifi networks.',buttonNegative: 'DENY',buttonPositive: 'ALLOW',},);if (!granted) {throw new Error('Missing Android permission');}}await Location.requestForegroundPermissionsAsync();const res = await WifiManager.getCurrentWifiSSID();setCurrentWifiSSID(res);} catch (e) {if (err instanceof Error) {Alert.alert('ERROR', err.message);}}};getInitialSSID();}, []);const checkIfConnectedToIoTWiFi = async () => {let tries = 0;await delay(1000);let res = await WifiManager.getCurrentWifiSSID();while (res !== ssid) {await delay(1000);res = await WifiManager.getCurrentWifiSSID();tries++;if (tries >= 10) {throw new Error('WiFi connection timed out.');}}setCurrentWifiSSID(res);};const startProvisioning = async () => {try {setLoading(true);await connectToSpecificSSID();await checkIfConnectedToIoTWiFi();await sendWifiCredentials();} catch (e) {if (err instanceof Error) {Alert.alert('ERROR', err.message);}} finally {setLoading(false);}};const connectToSpecificSSID = async () => {const isWep = false;const isHidden = false;await WifiManager.connectToProtectedSSID(ssid, password, isWep, isHidden);};const sendWifiCredentials = async () => {await fetch('https://192.168.4.1:1338/config', {method: 'POST',body: JSON.stringify({ssid: ssidMain,password: passwordMain,}),headers: {'content-type': 'application/json',},});};return (<Viewstyle={{flex: 1,paddingTop: 100,paddingHorizontal: 20,}}><Text>Current WiFi: {currentWifiSSID}</Text><Viewstyle={{ height: 1, backgroundColor: 'black', marginVertical: 10 }}/><Text>IOT WIFI CONFIG</Text><TextInputplaceholder="SSID"value={ssid}onChangeText={setSSID}style={{ borderWidth: 1, height: 60, marginTop: 20 }}/><TextInputplaceholder="PASSWORD"value={password}onChangeText={setPassword}secureTextEntrystyle={{ borderWidth: 1, height: 60, marginBottom: 20 }}/><Viewstyle={{ height: 1, backgroundColor: 'black', marginVertical: 10 }}/><Text>MAIN WIFI CONFIG</Text><TextInputplaceholder="SSID MAIN WIFI"value={ssidMain}onChangeText={setSSIDMain}style={{ borderWidth: 1, height: 60, marginTop: 20 }}/><TextInputplaceholder="PASSWORD MAIN WIFI"value={passwordMain}onChangeText={setPasswordMain}secureTextEntrystyle={{ borderWidth: 1, height: 60, marginBottom: 20 }}/>{loading ? (<ActivityIndicator size={'large'} />) : (<Button title="Connect" onPress={startProvisioning} />)}</View>);};export default WelcomeScreen;
As the process of disconnecting from your own WiFi to connect to the IoT’s WiFi is not instant and is asynchronous by nature, we’ll use a little function that will check the new SSID we are currently connected to every 1 sec and will throw if we can’t connect to the targeted WiFi within 10 sec (checkIfConnectedToIoTWiFi).
In order to use getCurrentWifiSSID on iOS, you’ll need to install expo-location and request the location permission first using requestForegroundPermissionsAsync
We’ll connect to an ESP32 running in AP mode for this example. I won’t get into much details about the ESP32 side of things for this article, but we’ll use this for it’s simplicity so somewhere in our ESP32 code we’ll have something like that: WiFi.softAP("testssid", "testpassword");. We’ll also use PsychicHttp for our HTTP/HTTPS server.
So testssid will be our IoT SSID and testpassword our IoT password and the /config endpoint of our ESP32 will receive our main WiFi credentials and attempt to join our main WiFi network.
Once the ESP32 would have connected to our main Wifi network, the AP mode will disengage and our phone should automatically reconnect to our previous WiFi (which in this case is our main wifi)
Code in action
I added a little button “test” that I call only at the very end of the whole process. This is just a button making an API call to my device with it’s new IP address once it’s on my main WiFi network. That’s just to validate that my IoT device is no longer in AP Mode and that it’s now really connected to my main WiFi network. The string “Hello from your main wifi network” comes directly from the web server running on my ESP32.
You can follow the process by looking at “Current Wifi” and “Current stage”. And here’s the logs on my ESP32:
AP IP address: 192.168.4.1 (IoT device in AP Mode)NEW SSID RECEIVED (Got the Wifi creds from /config)Hood-2G` (SSID received from POST on /config)NEW IP FROM MAIN NETWORK: 192.168.0.103 (IP address associated by my router to the ESP32)And that sums up one way of how you could build an IoT provisioning flow with React Native, Expo and JuanSeBestia/react-native-wifi-reborn. Happy Hacking!



