In this tutorial, you'll learn how to add tru.ID authentication using the SubscriberCheck API and Auth0’s social login to a Flutter application.
Auth0 is a platform that offers authorization and authentication strategies out of the box. It supports single-page web apps, regular web apps, and native apps. Auth0 does not have an officially supported SDK for Flutter, so this tutorial uses a third-party package called flutter_appauth
.
flutter_appauth
is a wrapper for AppAuth, a client SDK for native apps to authenticate and authorize end-users using OAuth 2.0 and OpenID Connect protocols.
Before you begin
To follow along with this tutorial, you'll need:
- A tru.ID Account
- An Auth0 Account
- Ngrok installed locally
- A mobile phone with a SIM card and mobile data connection
Getting started
In your terminal, clone the starter-files
branch of the tutorial repository with the following command:
git clone -b starter-files https://github.com/tru-ID/sim-based-authentication-flutter-auth0.git
If the finished code is what you're interested in, then the command below will clone the repository with the main
branch as the active branch:
git clone -b main https://github.com/tru-ID/sim-based-authentication-flutter-auth0.git
A tru.ID Account is needed to make the PhoneCheck API requests, so make sure you've created one.
You'll also need the tru.ID CLI to create a project and run a local server for development. To install this CLI, run the following command in your terminal:
npm install -g @tru_id/cli
Once installed, run the command below using your tru.ID credentials, which you can find in the tru.ID console:
tru setup:credentials <YOUR_TRU.ID_CLIENT_ID> <YOUR_TRU.ID_CLIENT_SECRET> <DATA_RESIDENCY>
Install the tru.ID CLI development server plugin. This plugin runs the dev-server
locally to allow your mobile application to send requests.
Create a new tru.ID project within the root directory with the following command:
tru projects:create flutter-auth0 --project-dir .
The development server is the backend server that the mobile application will communicate with for creating and validating PhoneChecks. To run the development server, enter the following command, pointing it to the directory containing the newly created project configuration. This command will run a local development server on port 8080
:
tru server
To make the development server publicly accessible on the Internet, which is needed for your application to communicate with the server, run the following in a new terminal:
ngrok http 8080
Getting started with Auth0
In your browser, sign in to your Auth0 dashboard. Once signed in, you'll be redirected to the dashboard, as shown below.

Create a new Auth0 application by clicking on the ‘applications’ pane on the left-hand side. This action will route you to a new page. Click on the ‘Create Application’ button to create a new application, as shown in the image below:

Creating an Auth0 application will give you Client ID
and Client Secret
values, found under the settings tab, to authenticate the user.
The next step is to set up the allowed callback URL, which is called during the authentication process. In the settings page, scroll to ‘Application URIs’, and in the ‘Allowed Callback URL’ section, enter the following:
com.truid.flutterauth0subscribercheck://login-callback
The screen you should see is displayed in the image below:

In your project directory, make a duplicate of the file .env.example
and call it .env
. Now within this file, you'll find some empty variables. Update the values of these variables to be your Auth0 Client ID and Auth0 Domain.
Note: The
AUTH0_ISSUER
is your domain with thehttps://
at the beginning.
Run the app
You've covered all of the initial setup steps; it's time to start the project. First, make sure you have a physical device connected. Once the device is connected and turned on, enter the following command in your terminal:
flutter run
Note For a physical iOS device, ensure you've provisioned your project in XCode.
On completion of the above command, you'll see a screen on your device similar to what's shown below:

Get the user's phone number
For the mobile application to communicate with the backend web server, the baseURL
value in lib/login.dart
needs to be updated. The value of this variable should be the ngrok
URL you noted earlier in the tutorial.
Next, the application needs somewhere to store the phone number and a boolean to determine whether an action is loading or not, usually triggered when the user has pressed the submit button.
Find the line class _LoginState extends State<Login>
, and add the following two variable declarations (phoneNumber
and loading
) below the line:
String phoneNumber = '';bool loading = false;
Adding the above two variables currently doesn't have any use in the application, so now you're going to add functionality for this. In the lib/login.dart
file, find the build
widget, and within this widget, add the following two new containers within your ListView
's children:
Container(child: Padding(padding:const EdgeInsets.symmetric(horizontal: 40, vertical: 10),child: TextField(keyboardType: TextInputType.phone,onChanged: (text) {setState(() {phoneNumber = text;});},decoration: const InputDecoration(border: OutlineInputBorder(),hintText: 'Enter your phone number.',),),),),Container(child: Padding(padding:const EdgeInsets.symmetric(horizontal: 10, vertical: 10),child: TextButton(onPressed: () async {}, child: const Text('Login')),),)
A visual indicator is needed to disable the button and display a loading image when the login has been submitted. Near the bottom, find the following line: onPressed: () async {}, child: const Text('Login')),
and update it as shown below:
child: TextButton(onPressed: () async {},child: loading? const CircularProgressIndicator(): const Text('Login'),),
In the code sample above, if the value of loading
is true
, then a CircularProgressIndicator
widget is displayed to act as the visual cue that something is happening.
Refresh your application on the mobile device and you'll see the screen shown in the example below:

Create reusable functions
There are two core reusable functions needed for this authentication process. These two functions handle the flow for the user depending on the outcome of the SubscriberCheck
. These are an errorHandler
, which will display an AlertDialog
widget containing the error. The second is the successHandler
function to return a generic AlertDialog
widget letting the user know of the successful check.
In lib/login.dart
, find the line class _LoginState extends State<Login>
, and add the following snippet of code which contains the two new functions for these possible outcomes:
Future<void> errorHandler(BuildContext context, String title, String content) {return showDialog(context: context,builder: (BuildContext context) {return AlertDialog(title: Text(title),content: Text(content),actions: <Widget>[TextButton(onPressed: () => Navigator.pop(context, 'Cancel'),child: const Text('Cancel'),),TextButton(onPressed: () => Navigator.pop(context, 'OK'),child: const Text('OK'),),],);});}Future<void> successHandler(BuildContext context) {return showDialog(context: context,builder: (BuildContext context) {return AlertDialog(title: const Text('Registration Successful.'),content: const Text('✅'),actions: <Widget>[TextButton(onPressed: () => Navigator.pop(context, 'Cancel'),child: const Text('Cancel'),),TextButton(onPressed: () => Navigator.pop(context, 'OK'),child: const Text('OK'),),],);});}
Create the SubscriberCheck
This next section makes an HTTP request to the reachability API, making sure the user's mobile network operator (MNO) supports the SubscriberCheck API. If so, the application triggers a PhoneCheck request. The tru.ID Flutter SDK contains all the functionality to provide this experience. Install this SDK in the terminal with the following command:
flutter pub add tru_sdk_flutter# or$ dart pub add tru_sdk_flutter
Now add the imports for tru.ID's SDK and dart's convert
library to the top of lib/login.dart
:
import 'package:tru_sdk_flutter/tru_sdk_flutter.dart';import 'dart:convert';
Still within the lib/login.dart
file, locate your last Controller view (which is the TextButton widget). Within this widget you'll see the following line: onPressed: () async {},
. This line is where the check is triggered when the user presses on the button. Replace this line with the following:
onPressed: () async {setState(() {loading = true;});TruSdkFlutter sdk = TruSdkFlutter();String? reachabilityInfo = await sdk.isReachable();ReachabilityDetails reachabilityDetails =ReachabilityDetails.fromJson(jsonDecode(reachabilityInfo!));if (reachabilityDetails.error?.status == 400) {return errorHandler(context, "Something Went Wrong.","Mobile Operator not supported.");}bool isSubscriberCheckSupported = false;if (reachabilityDetails.error?.status != 412) {isSubscriberCheckSupported = false;for (var products in reachabilityDetails.products!) {if (products.productName == "Subscriber Check") {isSubscriberCheckSupported = true;}}} else {isSubscriberCheckSupported = true;}if (isSubscriberCheckSupported) {} else {}},
The isReachable
function is called in the code above, which returns the fields networkId
, networkName
, countryCode
, and products
. The products
field is an optional array listing the products supported by the tru.ID and the MNO. You'll also find an error
object which may contain any potential errors.
In the next part, the application creates the variable isSubscriberCheckSupported
. If the HTTP status is not a 412
, the application loops through the products and checks whether the productName
equals Phone Check
; if it does, then isSubscriberCheckSupported
is set to true
.
If the isSubscriberCheckSupported
variable gets set to false
by the end of the loop, the SubscriberCheck is not supported. For now, nothing happens (but you'll fix that later in the tutorial). If the value of isSubscriberCheckSupported
is true
, the SubscriberCheck can proceed.
To handle the SubscriberCheck response, you need a file with the data model classes. A SubscriberCheck
API request returns the response with the fields check_id
and check_url
, so the SubscriberCheck
class will have two fields, checkId
and checkUrl
, which will be converted values from the API response. Create a new file named models.dart
inside your lib
directory and add the following:
import 'dart:convert';SubscriberCheck subscriberCheckFromJSON(String jsonString) =>SubscriberCheck.fromJson(json.decode(jsonString));class SubscriberCheck {String checkId;String checkUrl;SubscriberCheck({required this.checkId, required this.checkUrl});factory SubscriberCheck.fromJson(Map<String, dynamic> json) =>SubscriberCheck(checkId: json["check_id"], checkUrl: json["check_url"]);}
Now, within the lib/login.dart
file, add the import at the top of this file for your newly created models.dart
:
import 'package:flutterauth0subscribercheck/models.dart';
The next step is to create a function for creating the SubscriberCheck
resource. The HTTP package is needed to make HTTP network requests. In the terminal, run the following to install this package:
flutter pub add http#ordart pub add http
Back in lib/login.dart
file, at the top, add the import for this newly installed library:
import 'package:http/http.dart' as http;
A new function is needed that utilizes the newly imported library to take the user's phone number and make a POST
request to the endpoint /subscriber-check
. If the HTTP status is 200
, call the helper method from the SubscriberCheck
class to convert the JSON to an object. Otherwise, return null.
Above the line Future<void> errorHandler(BuildContext context, String title, String content) {
, add the following code to create the new function, createSubscriberCheck
:
Future<SubscriberCheck?> createSubscriberCheck(String phoneNumber) async {final response = await http.post(Uri.parse('$baseURL/subscriber-check'),body: {"phone_number": phoneNumber});if (response.statusCode != 200) {return null;}final String data = response.body;return subscriberCheckFromJSON(data);}
Now, add the functionality to call the function in the onPressed
handler. For this, locate the line if (isSubscriberCheckSupported) {
, and below it, add the following:
final SubscriberCheck? subscriberCheckResponse =await createSubscriberCheck(phoneNumber);if (subscriberCheckResponse == null) {setState(() {loading = false;});return errorHandler(context, 'Something went wrong.','Phone number not supported');}
The image below is an example of what will be shown if the MNO does not support the user’s phone number.

Open the Check URL
When the application creates a SubscriberCheck
, the response contains a check URL needed to make a GET
request over cellular for the check to succeed. The tru.ID Flutter SDK uses native functionality to force the network request through a cellular connection rather than WiFi.
Continuing to add more code from the last entry in the lib/login.dart
file, add the following:
// open check URLString? result =await sdk.check(subscriberCheckResponse.checkUrl);if (result == null) {setState(() {loading = false;});errorHandler(context, "Something went wrong.","Failed to open Check URL.");}
The code above first opens the check URL; it then checks whether a result was received, which will indicate whether the check was successful or not.
Get the SubscriberCheck result
This section covers handling the SubscriberCheck
result. To handle this result, a new data class is required to represent the properties expected in the SubscriberCheck
response.
At the bottom of lib/models.dart
, add the following SubscriberCheckResult
class containing two new fields, match
and simChanged
:
SubscriberCheckResult subscriberCheckResultFromJSON(String jsonString) =>SubscriberCheckResult.fromJson(json.decode(jsonString));class SubscriberCheckResult {bool match;bool simChanged;SubscriberCheckResult({required this.match, required this.simChanged});factory SubscriberCheckResult.fromJson(Map<String, dynamic> json) =>SubscriberCheckResult(match: json["match"], simChanged: !json["no_sim_change"]);}
The application now needs to make a request to GET
the SubscriberCheck
, passing in the check_id
as a route parameter. To make this request and use the newly created data class, open lib/login.dart
. Underneath the createSubscriberCheck
function, add the following getSubscriberCheck
function:
Future<SubscriberCheckResult?> getSubscriberCheck(String checkId) async {final response =await http.get(Uri.parse('$baseURL/subscriber-check/$checkId'));if (response.statusCode != 200) {return null;}final String data = response.body;return subscriberCheckResultFromJSON(data);}
Now, add the functionality to call the new function. Continuing from the code recently added to lib/login.dart
, (If you've lost this position, it's above the line: } else {}
, near the bottom of the file), add the following:
final SubscriberCheckResult? subscriberCheckResult =await getSubscriberCheck(subscriberCheckResponse.checkId);if (subscriberCheckResult == null) {// return dialogsetState(() {loading = false;});return errorHandler(context, 'Something Went Wrong.','Please contact support.');}if (subscriberCheckResult.match &&subscriberCheckResult.simChanged == false) {} else {setState(() {loading = false;});return errorHandler(context, "Something went wrong.","Unable to login. Please try again later");}
If the value of match
is true
and the value of simChanged
is false
, the user can proceed with Auth0 authentication. Otherwise, the application renders the errorHandler
displaying the error.
Authenticating with Auth0
Now that you've covered verifying the user is the owner of the phone number through tru.ID's SubscriberCheck, the application needs the functionality to authenticate with Auth0's social login. This tutorial uses Google as the social login. However, Auth0 covers many other options.
There are two different conditions of the authentication flow that determine whether the Auth0 call can be made. These conditions are:
- If the SubscriberCheck API isn't supported, the application will allow users to log in with Google through Auth0.
- If the SubscriberCheck returns a match and the SIM card has not recently changed, the user can log in with Google through Auth0.
The first step to this process is to add the following in the corresponding empty else block, near the end of the file:
final AuthorizationTokenResponse? result = await appAuth.authorizeAndExchangeCode(AuthorizationTokenRequest(dotenv.env["AUTH0_CLIENT_ID"]!,dotenv.env["AUTH0_REDIRECT_URI"]!,issuer: dotenv.env["AUTH0_ISSUER"]!,scopes: ['openid', 'profile', 'offline_access'],promptValues: ['login']));if (result?.idToken != null) {setState(() {loading = false;});return successHandler(context);} else {setState(() {loading = false;});return errorHandler(context, "Something went wrong.","Unable to login. Please try again later");}
Next, find the empty condition line if (subscriberCheckResult.match && subscriberCheckResult.simChanged == false) {}
, and within the {}
, add the following:
final AuthorizationTokenResponse? result = await appAuth.authorizeAndExchangeCode(AuthorizationTokenRequest(dotenv.env["AUTH0_CLIENT_ID"]!,dotenv.env["AUTH0_REDIRECT_URI"]!,issuer: dotenv.env["AUTH0_ISSUER"]!,scopes: ['openid', 'profile', 'offline_access'],promptValues: ['login']));if (result?.idToken != null) {setState(() {loading = false;});return successHandler(context);} else {setState(() {loading = false;});return errorHandler(context, "Something went wrong.","Unable to login. Please try again later");}
The images below show screenshots of a mobile phone progressing through the authentication flow, including the parts of the code you've implemented throughout this tutorial, and the parts that Auth0 handles.



Wrapping up
That's it! With everything in place, you now have a seamless login flow using Flutter, Auth0, and the tru.ID SubscriberCheck API.