Flutter is Google's cross-platform UI toolkit created to help developers build expressive and beautiful mobile applications. In this article, you will learn how to build and secure a Flutter application with Auth0 using the open-source AppAuth library with the flutter_appauth
wrapper plugin. You can check out the code developed throughout the article in this GitHub repository.
🙏 Special thanks to Majid Hajian for offering his time and expertise to review this blog post and its sample app. Majid's feedback helped us ensure that we are providing high-quality content to the Flutter community.
Prerequisites
Before getting started with this article, you need a working knowledge of Flutter. If you need help getting started, you can follow the codelabs on the Flutter website.
You also need to have the following installations in your machine:
- Flutter SDK: We tested this tutorial with SDK version 1.17.
A Development Environment, one of:
These IDEs integrate well with Flutter and make your development effective through the provision of tools to edit and refactor your Flutter application code. You will need an installation of the Dart and Flutter plugins, regardless of the IDE you decide to use.
OAuth 2.0 Flow and Mobile Applications
OAuth 2.0 is an industry-standard protocol for authorization. It allows users to give third-party applications access to their resources. You can see a typical example of OAuth 2.0 in action when a user tries to sign up for a third-party app using Google. OAuth 2.0 allows users to give the third-party application access to resources, such as using their profile data on a social network platform, without needing to input their credentials on said application.
OpenID Connect (OIDC) is an authentication protocol on top of OAuth 2.0. It expands the successful delegation model of OAuth 2.0 in many ways, like the ability to sign-in, a JWT structured ID token, and discovery.
OAuth 2.0 is not just for web applications. It provides different flows to address authentication requirements for various types of applications. For mobile applications, OAuth 2.0 provides the Authorization Code Grant flow with PKCE, which is the recommended flow that you'll use throughout this tutorial.
A significant benefit of using standards like OAuth 2.0 and OIDC is that you can decouple your application from a particular vendor. You may have different options of open-source software libraries that can help you integrate your application with these two protocols — you don't have to start from scratch.
For your Flutter application, you can delegate that integration job to AppAuth, a standard library for OAuth 2.0. Although Auth0 does not maintain this library, it works flawlessly with Auth0.
What You'll Build
Throughout this article, you'll build an application that allows users to log in or sign up using a social identity provider, such as Google, or a set of credentials, such as a username and password. You won't have to build any forms, though! The application will leverage a login page provided by Auth0, the Universal Login page. Your application will also have a profile screen where you can display detailed information about the logged-in user and a logout button.
Take a peek of what you'll build:
If you encounter any issues, the complete source code of the sample application is available on this GitHub repository.
Scaffold a Flutter project
To facilitate the process of creating a new Flutter project, you will use the Flutter CLI tool. To do this, open your terminal and navigate to your projects directory to run the following command:
flutter create --org com.auth0 flutterdemo
The
com.auth0
parameter sets the hierarchy of your Flutter app, which is significant when you are implementing user authentication using a callback URL. You'll find more details on this concept, as you follow the article.
The CLI tool generates a template project within a couple of seconds to get you started, which you can open in your preferred IDE.
Open the lib/main.dart
file and replace its entire content with following code template:
/// -----------------------------------
/// External Packages
/// -----------------------------------
import 'package:flutter/material.dart';
/// -----------------------------------
/// Auth0 Variables
/// -----------------------------------
/// -----------------------------------
/// Profile Widget
/// -----------------------------------
/// -----------------------------------
/// Login Widget
/// -----------------------------------
/// -----------------------------------
/// App
/// -----------------------------------
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
_MyAppState createState() => _MyAppState();
}
/// -----------------------------------
/// App State
/// -----------------------------------
class _MyAppState extends State<MyApp> {
bool isBusy = false;
bool isLoggedIn = false;
String errorMessage;
String name;
String picture;
Widget build(BuildContext context) {
return MaterialApp(
title: 'Auth0 Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Auth0 Demo'),
),
body: Center(
child: Text('Implement User Authentication'),
),
),
);
}
}
This template is the skeleton of your app. You'll add code to each section as you follow the article.
Install Dependencies
This Flutter project requires three main dependencies:
http: A composable, Future-based library for making HTTP requests published by the Dart Team.
flutter_appauth : A well-maintained wrapper package around AppAuth for Flutter developed by Michael Bui. AppAuth authenticates and authorizes users and supports the PKCE extension.
fluttersecurestorage: A library to securely persist data locally developed by German Saprykin.
Next, open the pubspec.yaml
file located under the project root directory. Specify your project dependencies by replacing the dependencies
section with the snippet below:
dependencies:
flutter:
sdk: flutter
http: ^0.12.1
flutter_appauth: ^0.9.1
flutter_secure_storage: ^3.3.3
Then, Click "Pub get" in your IDE or run the following command in the project root to download the dependencies.
flutter pub get
Configure Dependencies and Callback URL
flutter_appauth
is a package that wraps around the AppAuth
native libraries. It provides access to the methods required to perform user authentication, following the standards that Auth0 also happens to implement. To build a communication bridge between your Flutter app and Auth0, you need to set up a callback URL to receive the authentication result in your application after a user logs in with Auth0.
A callback URL is a mechanism by which an authorization server communicates back to your application.
For web applications, the callback URL is a valid HTTP(s) URL. More or less, the same applies to native applications. The subtle difference is that in the native applications, callbacks are sudo-URLs that you compose using an application schema and URI that's configured per application.
Throughout this demo, we'll use the value of com.auth0.flutterdemo://login-callback
for the callback URL. How you set this value depends on what mobile operating system you are supporting, Android or iOS.
flutter_appauth
will register your app with an intent filter on that callback URL and, if there's no match, the result is not received in the app.
You also need to tweak the Android build system to work with flutter_secure_storage
.
Configure Android Dependencies and Callback URL
flutter_secure_storage
has a minSdkVersion:18
dependency, so you need to bump up the default minSdkVersion:16
provisioned by the flutter create
scaffolding command.
Update the android/app/build.gradle
file as follows:
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.auth0.flutterdemo"
minSdkVersion 18
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
manifestPlaceholders = [
'appAuthRedirectScheme': 'com.auth0.flutterdemo'
]
}
Notice the added lines to insert the appAuthRedirectScheme
variable into your defaultConfig
section. The value of appAuthRedirectScheme
must be in lower case letters.
Configure iOS Callback URL
iOS default settings work with the project dependencies without any modifications. You can set the callback scheme by adding the following entry to the <dict>
element present in the ios/Runner/Info.plist
file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
...
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.auth0.flutterdemo</string>
</array>
</dict>
</array>
</dict>
</plist>
Run the Application
Launch either the iOS simulator or Android emulators, then run the application on all available devices like so:
flutter run -d all
I have feedback or ran into an issue
Create the User Interface
Locate the Profile Widget
section in the lib/main.dart
file and create the following widget:
/// -----------------------------------
/// Profile Widget
/// -----------------------------------
class Profile extends StatelessWidget {
final logoutAction;
final String name;
final String picture;
Profile(this.logoutAction, this.name, this.picture);
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 150,
height: 150,
decoration: BoxDecoration(
border: Border.all(color: Colors.blue, width: 4.0),
shape: BoxShape.circle,
image: DecorationImage(
fit: BoxFit.fill,
image: NetworkImage(picture ?? ''),
),
),
),
SizedBox(height: 24.0),
Text('Name: $name'),
SizedBox(height: 48.0),
RaisedButton(
onPressed: () {
logoutAction();
},
child: Text('Logout'),
),
],
);
}
}
This widget defines a view that displays user profile information once the user has logged in. It also displays a logout button.
Locate the Login Widget
section and create the following widget:
/// -----------------------------------
/// Login Widget
/// -----------------------------------
class Login extends StatelessWidget {
final loginAction;
final String loginError;
const Login(this.loginAction, this.loginError);
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () {
loginAction();
},
child: Text('Login'),
),
Text(loginError ?? ''),
],
);
}
}
This widget defines a view that your app shows to users who have not been authenticated yet by Auth0. It displays a login button so that they can start the authentication process.
Set Up Auth0
Auth0 is an Identity-as-a-Service (IDaaS) platform that provides developers with features such as Social and Passwordless Login, among others, to ease online identity management.
To integrate Auth0 into your Flutter app, you need an Auth0 account. If you have an existing account, you can use it. If you don't, click here to create a free account.
After creating an Auth0 account, follow the steps below to set up an application:
- Go to the Applications section of your dashboard.
- Click on the "Create Application" button.
- Enter a name for your application (e.g., "Flutter Application").
- Finally, select Native as the application type and click the Create button.
Your application should have at least one enabled Connection. Click on the "Connections" tab on your application page and switch on any database or social identity provider (e.g., Google).
Finally, navigate to the "Settings" tab on your application page and set a callback URL in the Allowed Callback URLs field. For this demo, your callback URL should be the following value:
com.auth0.flutterdemo://login-callback
Here is how it should look in your Application settings page:
Once you set the callback URL value, scroll to the bottom of the page and click on the "Save Changes" button. You should receive a confirmation message stating that your changes have been saved.
As mentioned earlier, the purpose of the callback URL is to provide a mechanism by which an authorization server communicates back to your Flutter application.
A word for Android developers from Luciano Balmaceda, Auth0 Mobile Engineer, on how applicationId
and appAuthRedirectScheme
relate to the callback URL.
The applicationId
is the package name of the Android app. It can have lowercase or uppercase characters. However, the standard practice is to use lowercase characters.
Because the generated callback URL includes this package name and callback URLs are case-sensitive, any casing mismatch would prevent this intent from taking the result back to the app, producing an infinite loop or perpetual "loading" state.
In the next sections, you'll create a callback URL The scheme of the URL you register in the "Allowed Callback URLs" section of your Auth0 application must match the value of of appAuthRedirectScheme
.
In addition, intent filters configured in the Android manifest to listen for a callback URL need to have their scheme part in lowercase.
Integrate Auth0 with Flutter
Auth0 is a standard OAuth 2.0 authorization server. Developers can utilize any universal OAuth 2.0 or OIDC SDK to authenticate against Auth0. Currently, there is no official Flutter SDK for Auth0. However, you're going to use the AppAuth Native SDK via the flutter_appauth wrapper to integrate user authentication in your application.
Locate the External Packages
section in the lib/main.dart
file and update it as follows:
/// -----------------------------------
/// External Packages
/// -----------------------------------
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
final FlutterAppAuth appAuth = FlutterAppAuth();
final FlutterSecureStorage secureStorage = const FlutterSecureStorage();
Next, locate the Auth0 Variables
section and update it like so:
/// -----------------------------------
/// Auth0 Variables
/// -----------------------------------
const AUTH0_DOMAIN = 'YOUR-AUTH0-DOMAIN';
const AUTH0_CLIENT_ID = 'YOUR-AUTH0-CLIENT-ID';
const AUTH0_REDIRECT_URI = 'com.auth0.flutterdemo://login-callback';
const AUTH0_ISSUER = 'https://$AUTH0_DOMAIN';
The Authorization Code Grant flow with PKCE doesn't require a client secret. You only need to use your Auth0 Domain and Auth0 Client ID in the lib/main.dart
file to specify to which Tenant (Domain) and Application (Client ID) from Auth0 the Flutter app should connect. You may have multiple tenants and several applications registered at Auth0. Hence, it's important to specify them.
Use the values of Domain and Client ID from your Application settings as the values of AUTH0_DOMAIN
and AUTH0_CLIENT_ID
.
Integration with AppAuth
The very first step in setting up AppAuth against your authorization server is to configure OAuth 2.0 endpoint URLs. Your sample application involves three endpoints:
Authorization endpoint: You use it to start the redirect-based login and receive an authorization code in the callback. In Auth0, its value is
https://TENANT.auth0.com/authorize
.Token endpoint: You use it to exchange an authorization code or refresh token for new access and ID tokens. In Auth0, its value is
https://TENANT.auth0.com/oauth/token
.Userinfo endpoint: You use it to retrieve user profile information from the authorization server. In Auth0, its value is
https://TENANT.auth0.com/userinfo
.
OpenID Connect is a protocol for authentication based on OAuth 2.0. OpenID Connect introduced OpenID Connect Discovery as a standard way to discover authorization server endpoints in a JSON document. In Auth0, you can find the discovery document at the /.well-known/openid-configuration
endpoint of your tenant address. For this demo, that's https://YOUR-AUTH0-TENANT-NAME.auth0.com/.well-known/openid-configuration
.
AppAuth supports three methods to configure endpoints. Most conveniently, you just pass the top-level domain name (i.e., issuer
) as a parameter to AppAuth methods. AppAuth then internally fetches the discovery documents from the openid-configuration
endpoint and figures out where to send subsequent requests.
With that said, let's proceed and implement methods to manage user authentication in the _MyAppState
widget class, which should look like this when you are done:
class _MyAppState extends State<MyApp> {
bool isBusy = false;
bool isLoggedIn = false;
String errorMessage;
String name;
String picture;
Widget build(BuildContext context) {...}
Map<String, dynamic> parseIdToken(String idToken) {...}
Future<Map> getUserDetails(String accessToken) async {...}
Future<void> loginAction() async {...}
void logoutAction() async {...}
void initState() {...}
}
Locate the App State
section and add the following methods in the order in which they are presented to the _MyAppState
widget class to avoid crashing your Flutter app:
Handle the ID Token with parseIdToken
Your Flutter application will get an ID token that it will need to parse as a Base64 encoded string into a Map
object. You'll perform that action inside the parseIdToken()
method.
Check this JSON payload to get a better sense of what a decoded ID token looks like:
{
"given_name": "Amin",
"family_name": "Abbaspour",
"nickname": "a.abbaspour",
"name": "Amin Abbaspour",
"picture": "https://lh3.googleusercontent.com/a-/AOh14GglAu_nSbRx6Wd5RBdN_tcH2xq0bFAaiVr9lPQCsyg",
"locale": "en",
"updated_at": "2020-05-29T04:55:44.158Z",
"email": "XXX@gmail.com",
"email_verified": true,
"iss": "https://flutterdemo.auth0.com/",
"sub": "google-oauth2|XXX",
"aud": "1b1NvfMVq6DP621IvegS7RB8XAsKD049",
"iat": 1590728144,
"exp": 1590764144,
"nonce": "mynonce"
}
Unlike an accessToken
, which is opaque for clients and should be consumed by APIs, OpenID Connect clients have the responsibility of validating the idToken
they receive. Fortunately, the AppAuth SDK does that for you; hence you can skip the validation and just decode the body.
Implement parseIdToken()
as a method of the _MyAppState
class as follows:
Map<String, dynamic> parseIdToken(String idToken) {
final parts = idToken.split(r'.');
assert(parts.length == 3);
return jsonDecode(
utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))));
}
There is a lot more ground to cover about JSON Web Tokens (JWTs) beyond the scope of this article. If you're interested in learning more, a great online resource is the Auth0 JWT Handbook.
Interested in getting up-to-speed with JWTs as soon as possible?
Download the free ebookRetrieve user profile information with getUserDetails
You explored the idToken
in the previous section and fetched the user's full name from the name
claim. One other attribute that you need to render in your profile screen is the user's picture.
You might have noticed that the picture URL is also part of the idToken
JSON object. To demonstrate an alternative way of fetching user profile information, you're going to implement a getUserDetails()
method. This method takes an accessToken
and sends it as a bearer authorization header to the /userinfo
endpoint. The result is a JSON object that's parsed and returned in a Future<Map>
object.
Implement the getUserDetails()
method as follows:
Future<Map<String, dynamic>> getUserDetails(String accessToken) async {
final url = 'https://$AUTH0_DOMAIN/userinfo';
final response = await http.get(
url,
headers: {'Authorization': 'Bearer $accessToken'},
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to get user details');
}
}
While its usage is limited to fetching user details in this article, the accessToken
should be kept alive throughout the lifecycle of large applications where it's needed to make frequent API calls. We'll leave that improvement to enthusiastic readers. Check out getTokenSilently()
method code to give you a hint on how to implement accessToken
caching in JavaScript.
Add user login with loginAction
The single method appAuth.authorizeAndExchangeCode()
handles the end-to-end flow: from starting a PKCE authorization code flow to getting authorization code in the callback and exchanging it for a set of artifact tokens.
Implement a loginAction()
method as follows:
Future<void> loginAction() async {
setState(() {
isBusy = true;
errorMessage = '';
});
try {
final AuthorizationTokenResponse result =
await appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(
AUTH0_CLIENT_ID,
AUTH0_REDIRECT_URI,
issuer: 'https://$AUTH0_DOMAIN',
scopes: ['openid', 'profile', 'offline_access'],
// promptValues: ['login']
),
);
final idToken = parseIdToken(result.idToken);
final profile = await getUserDetails(result.accessToken);
await secureStorage.write(
key: 'refresh_token', value: result.refreshToken);
setState(() {
isBusy = false;
isLoggedIn = true;
name = idToken['name'];
picture = profile['picture'];
});
} catch (e, s) {
print('login error: $e - stack: $s');
setState(() {
isBusy = false;
isLoggedIn = false;
errorMessage = e.toString();
});
}
}
A lot is going on here. Let's uncover it step by step.
First, we create an AuthorizationTokenRequest
object by passing it a few parameters.
The clientID
and redirectUrl
are mandatory parameters and correspond to the AUTH0_CLIENT_ID
and AUTH0_REDIRECT_URI
values, respectively.
The issuer
parameter enables the endpoints discovery, as discussed in the previous section.
The scopes
parameter defines the specific actions that a user allows the application to perform on the user's behalf. With the three scopes that you are passing, you request permission to:
- perform an
openid
connect sign-in, - retrieve user
profile
, - retrieve a refresh token for
offline_access
from the application.
Then, you start a sign-in transaction by passing the AuthorizationTokenRequest
object to appAuth.authorizeAndExchangeCode()
.
Upon completion of the sign-in transaction, the users authenticate with the authorization server and return to the application.
Inside the AuthorizationTokenResponse
result
object, you receive three tokens:
accessToken
: an OAuth 2.0 artifact that allows the application to call secure APIs on behalf of the user.idToken
: user profile information in JWT format.refreshToken
: a token to obtain a newaccessToken
andidToken
.
It's worth mentioning that in real-world scenarios, you'll have more scopes, depending on your app functionality. For example, an application that allows users to list and edit their Spotify library requires the user-library-read
and user-library-modify
scopes.
In a future article, we'll show you how to configure Auth0 to call third party APIs from Flutter applications.
You also use the previously defined parseIdToken()
to get the ID Token and getUserDetails()
to get the user profile information. Finally, you use secureStorage.write()
to store the value of the refreshToken
token locally so that you can streamline the login user experience — you'll see how that works in the next sections.
Add user logout with logoutAction
Logout is simply implemented as follows:
void logoutAction() async {
await secureStorage.delete(key: 'refresh_token');
setState(() {
isLoggedIn = false;
isBusy = false;
});
}
The logoutAction()
method first removes any refreshToken
from storage, then changes the isLoggedIn
state back to false
.
This simple logout method does not remove the authorization server (AS) session from the browser. That means that, depending on the validity of the AS session, next time you hit "Login", the whole redirecting to browser and back could be a seamless experience without any login prompt! That might not be a considerable concern for a personal device, but it's a concern for shared devices.
While a complete secure logout is beyond the scope of this article, let me mention that you can request an interactive login in the Authorization Server by passing an additional prompt=login
parameter within the loginAction()
method by uncommenting the promptValues
line from the definition of its result
variable:
Future<void> loginAction() async {
setState(() { ... });
try {
final AuthorizationTokenResponse result =
await appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(
AUTH0_CLIENT_ID,
AUTH0_REDIRECT_URI,
issuer: 'https://$AUTH0_DOMAIN',
scopes: ['openid', 'profile', 'offline_access'],
promptValues: ['login'] // ignore any existing session; force interactive login prompt
),
);
final idToken = parseIdToken(result.idToken);
final profile = await getUserDetails(result.accessToken);
await secureStorage.write(
key: 'refresh_token', value: result.refreshToken);
setState(() { ... });
} catch (e, s) { ... }
}
Handle the user authentication state with initAction
and initState
You started the Authorization Code flow with the offline_access
scope. That means there is an additional refreshToken
returned from the token endpoint during authentication.
You use a refresh token to obtain new access and ID tokens even if the user is no longer signed in to the authorization server. By using refresh tokens, you don't need to re-authenticate your users whenever they launch the app. Instead, if there is any refresh token available, you can use it to get a new access token silently.
You should store refresh tokens securely alongside the application. flutter_secure_storage
is a library that exposes simple CRUD operations to store and retrieve sensitive application data.
When you start the application, the initState()
method checks if there is any existing refreshToken
. If so, it tries to retrieve a new accessToken
by calling the appAuth.token()
method. Here are the methods to add to _MyAppState
class:
void initState() {
initAction();
super.initState();
}
void initAction() async {
final storedRefreshToken = await secureStorage.read(key: 'refresh_token');
if (storedRefreshToken == null) return;
setState(() {
isBusy = true;
});
try {
final response = await appAuth.token(TokenRequest(
AUTH0_CLIENT_ID,
AUTH0_REDIRECT_URI,
issuer: AUTH0_ISSUER,
refreshToken: storedRefreshToken,
));
final idToken = parseIdToken(response.idToken);
final profile = await getUserDetails(response.accessToken);
secureStorage.write(key: 'refresh_token', value: response.refreshToken);
setState(() {
isBusy = false;
isLoggedIn = true;
name = idToken['name'];
picture = profile['picture'];
});
} catch (e, s) {
print('error on refresh token: $e - stack: $s');
logoutAction();
}
}
Note that initAction()
renews accessToken
regardless of the validity of any existing access token. You can further optimize this code by keeping track of accessTokenExpirationDateTime
and request a new accessToken
only if the one at hand is expired.
Render the user interface conditionally in build
Finally, update the build()
method as follows:
build(BuildContext context) {
return MaterialApp(
title: 'Auth0 Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Auth0 Demo'),
),
body: Center(
child: isBusy
? CircularProgressIndicator()
: isLoggedIn
? Profile(logoutAction, name, picture)
: Login(loginAction, errorMessage),
),
),
);
}
Widget
I have feedback or ran into an issue
Test the Final Application
Well done on getting to the final stage. If you successfully followed the steps so far, you should see a login screen similar to this one in your emulator:
Go ahead and tap the "Login" button. Note that in iOS, a consent prompt comes up to notify you that the application is intending to use the system browser SSO to process the login:
The iOS prompt is an expected part of the
ASWebAuthenticationSession
implementation.That should take you to the Auth0 Universal Login page in the system browser:
On this screen, either enter your credentials or click "Sign in with Google". Either way, once you successfully log in, the profile screen renders:
You can create new users in your tenant directly by using the "Users" section of the Auth0 Dashboard
Tapping the "Logout" button should take you back to the initial login screen.
Also, try terminating the application while you are logged in and rerunning it. Once the application loads up again, it should use the refreshToken
to take you straight into the profile screen without asking you to enter your credentials again.
Congratulations. You did it!
Conclusion and Recommendations
In this post, you learned how to secure a Flutter application with Auth0 using readily available OSS libraries. It didn't take you more than a couple of lines to connect and secure your application.
The article is intentionally simple to cover the basic flow. In a future article, we'll cover how to secure multi page apps as well as define and call back-end APIs from your Flutter application.