Integrate Your Flutter Application With FusionAuth
In this article, you are going to learn how to integrate a Flutter application with FusionAuth.
Here’s a typical application login flow before integrating FusionAuth into your Flutter application.
Here’s the same application login flow when FusionAuth is introduced.
Prerequisites
For this tutorial, you’ll need to have some of these tools installed.
- Flutter
- If you want to develop applications for iOS:
- Xcode
- iPhone simulator or real iOS device
- iPhone development environment
- If you want to develop applications for Android:
- Android emulator or real Android device
- Android development environment
You'll also need Docker, since that is how you’ll install FusionAuth.
The commands below are for macOS, but are limited to mkdir
and cd
, which have equivalents in Windows and Linux.
To make sure your environment is working correctly, run the following command in your terminal window.
flutter doctor
If everything is configured properly, you will see something like the following result in your terminal window:
Doctor summary (to see all details, run flutter doctor -v):
[âś“] Flutter (Channel stable, 3.7.10)
[âś“] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
[âś“] Xcode - develop for iOS and macOS (Xcode 14.3)
[âś“] Chrome - develop for the web
[âś“] Android Studio (version 2022.2)
[âś“] IntelliJ IDEA Community Edition (version 2022.2)
[âś“] Connected device (2 available)
[âś“] HTTP Host Availability
• No issues found!
Download and Install FusionAuth
First, make a project directory:
mkdir integrate-fusionauth && cd integrate-fusionauth
Then, install FusionAuth:
curl -o docker-compose.yml https://raw.githubusercontent.com/FusionAuth/fusionauth-containers/master/docker/fusionauth/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/FusionAuth/fusionauth-containers/master/docker/fusionauth/.env
docker compose up -d
Create a User and an API Key
Next, log into your FusionAuth instance. You’ll need to set up a user and a password, as well as accept the terms and conditions.
Then, you’re at the FusionAuth admin UI. This lets you configure FusionAuth manually. But for this tutorial, you're going to create an API key and then you’ll configure FusionAuth using our JavaScript client library.
Navigate to + button to add a new API Key. Copy the value of the Key field and then save the key.
It might be a value like CY1EUq2oAQrCgE7azl3A2xwG-OEwGPqLryDRBCoz-13IqyFYMn1_Udjt
.
Doing so creates an API key that can be used for any FusionAuth API call. Save that key value off as you’ll be using it later.
Set up a Public URL for Fusionauth
Your FusionAuth instance is now running on a different machine (your computer) than the mobile app will run (either a real device or an emulator), which means that it won't be able to access localhost
.
If the device (either real or emulator) and your computer are connected to the same network, you can use the local IP Address for your machine (e.g. 192.168.15.2
). Here are a few articles to help your find your IP Address depending on the Operational System you are running:
If they are not connected to the same network or if you have something that blocks connections (like a Firewall), learn how to expose a local instance to the Internet.
In either case, you'll have to use that address when configuring your instance and developing the app.
Configure FusionAuth
Next, you need to set up FusionAuth. This can be done in different ways, but we’re going to use the JavaScript client library. The below instructions use npm
on the command line, but you can use the client library with an IDE of your preference as well.
First, make a directory:
mkdir setup-fusionauth && cd setup-fusionauth
Now, copy and paste the following file into package.json
.
{
"name": "fusionauth-example-typescript-client",
"version": "1.0.0",
"description": "Example of setting up an application with typescript.",
"scripts": {
"setup-angular": "node setup-angular.js",
"setup-express": "node setup-express.js",
"setup-flutter": "node setup-flutter.js",
"setup-react": "node setup-react.js"
},
"author": "Dan Moore",
"license": "Apache-2.0",
"dependencies": {
"@fusionauth/typescript-client": "^1.45.0"
}
}
Install the dependencies.
npm install
Then copy and paste the following file into setup.js
. This file uses the FusionAuth API to configure your FusionAuth instance to allow for easy integration.
const {FusionAuthClient} = require('@fusionauth/typescript-client');
APPLICATION_ID = "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e";
RSA_KEY_ID = "356a6624-b33c-471a-b707-48bbfcfbc593"
// You must supply your API key as an environment variable
const fusionAuthAPIKey = process.env.fusionauth_api_key;
if (! fusionAuthAPIKey ) {
console.log("please set api key in the fusionauth_api_key environment variable")
process.exit(1)
}
// You must also supply the public URL for your FusionAuth instance
// Tip: you can use ngrok to expose a local application to the internet
const fusionAuthDomain = process.env.fusionauth_domain;
if (! fusionAuthDomain ) {
console.log("Please set the public URL in the fusionauth_domain environment variable");
process.exit(1);
}
if ((!fusionAuthDomain.startsWith('http://')) && (!fusionAuthDomain.startsWith('https://'))) {
console.log("The fusionauth_domain environment variable should start with http:// or https://");
process.exit(1);
}
async function getTenant(client) {
tenant = null
try {
clientResponse = await client.retrieveTenants()
tenant = clientResponse.response.tenants[0]
} catch (error) {
console.log("couldn't find tenants " + JSON.stringify(error))
process.exit(1)
}
return tenant
}
async function patchTenant(client, tenant) {
try {
clientResponse = await client.patchTenant(tenant["id"], {"tenant": {"issuer": fusionAuthDomain}})
} catch (error) {
console.log("couldn't update tenant " + JSON.stringify(error))
process.exit(1)
}
}
async function generateKey(client) {
try {
clientResponse = await client.generateKey(RSA_KEY_ID, {"key": {"algorithm":"RS256", "name":"For FlutterExampleApp", "length": 2048}})
} catch (error) {
console.log("couldn't create RSA key " + JSON.stringify(error))
process.exit(1)
}
}
async function createApplication(client) {
application = {}
application["name"] = "FlutterExampleApp"
application["oauthConfiguration"] = {}
application["oauthConfiguration"]["authorizedRedirectURLs"] = [
"com.fusionauth.flutterdemo://login-callback",
"com.fusionauth.flutterdemo://logout-callback"
]
application["oauthConfiguration"]["requireRegistration"] = true
application["oauthConfiguration"]["enabledGrants"] = ["authorization_code", "refresh_token"]
application["oauthConfiguration"]["clientSecret"] = "change-this-in-production-to-be-a-real-secret"
application["oauthConfiguration"]["clientAuthenticationPolicy"] = "NotRequiredWhenUsingPKCE";
application["oauthConfiguration"]["proofKeyForCodeExchangePolicy"] = "Required";
// assign key from above to sign our tokens. This needs to be asymmetric
application["jwtConfiguration"] = {}
application["jwtConfiguration"]["enabled"] = true
application["jwtConfiguration"]["accessTokenKeyId"] = RSA_KEY_ID
application["jwtConfiguration"]["idTokenKeyId"] = RSA_KEY_ID
try {
clientResponse = await client.createApplication(APPLICATION_ID, {"application": application})
} catch (error) {
console.log("couldn't create application " + JSON.stringify(error))
process.exit(1)
}
}
async function getUser(client) {
user = null
try {
// should only be one user
clientResponse = await client.searchUsersByQuery({"search": {"queryString":"*"}})
user = clientResponse.response.users[0]
} catch (error) {
console.log("couldn't find user " + JSON.stringify(error))
process.exit(1)
}
return user
}
// patch the user to make sure they have a full name, otherwise OIDC has issues
// TODO test check for errorResponse
async function patchUser(client, user) {
try {
clientResponse = await client.patchUser(user["id"], {"user": {"fullName": user["firstName"]+" "+user["lastName"]}})
} catch (error) {
console.log("couldn't patch user " + JSON.stringify(error))
process.exit(1)
}
}
async function registerUser(client, user) {
try {
clientResponse = await client.register(user["id"], {"registration":{"applicationId":APPLICATION_ID}})
} catch (error) {
console.log("couldn't register user " + JSON.stringify(error))
process.exit(1)
}
}
async function main(client) {
tenant = await getTenant(client)
await patchTenant(client, tenant)
await generateKey(client)
await createApplication(client)
user = await getUser(client)
await patchUser(client, user)
await registerUser(client, user)
console.log(user)
}
const client = new FusionAuthClient(fusionAuthAPIKey, fusionAuthDomain);
main(client)
Then, you can run the setup script, replacing YOUR_API_KEY_FROM_ABOVE
with the API Key you generated earlier and YOUR_PUBLIC_URL
with the public address for your instance (either http://your-ip-address:9011
or the URL ngrok provided you).
fusionauth_api_key=YOUR_API_KEY_FROM_ABOVE fusionauth_domain=YOUR_PUBLIC_URL npm run setup-flutter
If you are using PowerShell, you will need to set the environment variables in a separate command before executing the script.
$env:fusionauth_api_key='YOUR_API_KEY_FROM_ABOVE'
$env:fusionauth_domain='YOUR_PUBLIC_URL'
npm run setup-flutter
If you want, you can log into your instance and examine the new Application the script created for you.
Create Your Flutter Application
In order to create and set up the new Flutter app, run the following command. You'll want to be in a directory where the project should live.
mkdir ../flutter-app && cd ../flutter-app
flutter create fusionauth_demo
After the installation process completes, you will see that the fusionauth_demo
directory contains all the Flutter starter app configuration. Open the project directory with the text editor of your choice.
You can run your new project in the actual device or an emulator to confirm everything is working before you customize any code. Do so by running the following command in the project directory:
cd fusionauth_demo
flutter run
After running this, you will get the list of emulators or devices in which you want to run the project. You can simply choose the one in which you want the code to run. Make sure to remain in this directory for the rest of this tutorial.
However, to run the project on both iOS and Android together, you can use the following command:
flutter run -d all
The build process always takes a while the first time an app is built. After the successful build, you will get the boilerplate Flutter app running in your emulators.
Now that you have a basic working application running, let's jump into the fun stuff: adding auth!
Integrating AppAuth
AppAuth is a popular OAuth package that can be used in both native and cross-platform mobile applications. In this project, you will be storing the access token using the secure storage package. Since such tokens allow your application to access protected resources such as APIs, you need to take care they are stored as securely as possible.
Therefore, you need to install some dependencies to your project. For that, you need to replace the pubspec.yaml
file in your project with the one below.
name: fusionauth_demo
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: '>=2.19.6 <3.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
http: ^0.13.6
flutter_appauth: ^6.0.0
flutter_secure_storage: ^8.0.0
simple_gravatar: ^1.1.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
Setting Up AppAuth
Now that you have installed your libraries, you need to configure and add your previously configured callback URL to native configuration in both the Android and iOS directories.
Let's look at Android first.
Android Setup
In your editor, you need to go to the android/app/build.gradle
file for your Android app to specify the custom scheme. There should be a part of the file that looks similar to the code block below, but you'll need to add the FusionAuth URL com.fusionauth.flutterdemo
to the appAuthRedirectScheme
section.
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.fusionauth_demo"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 18
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
manifestPlaceholders += [
'appAuthRedirectScheme': 'com.fusionauth.flutterdemo'
]
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
iOS Setup
You need to edit the ios/Runner/Info.plist
file in the iOS app to specify the custom scheme. There should be a section in it that looks similar to the following, but you'll need to add the FusionAuth URL: com.fusionauth.flutterdemo://login-callback
.
At the end of your editing, make sure the CFBundleURLSchemes
section looks similar to this:
<?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>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>fusionauth_demo</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.fusionauth.flutterdemo</string>
</array>
</dict>
</array>
</dict>
</plist>
Dive into the code
You need to open the main.dart
file present inside the lib
directory of your project and paste the contents below.
Putting all your logic in one file makes sense for a tutorial, but for a larger application you'll probably want to split it up.
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:simple_gravatar/simple_gravatar.dart';
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();
const FlutterSecureStorage secureStorage = FlutterSecureStorage();
/// For a real-world app, this should be an Internet-facing URL to FusionAuth.
/// If you are running FusionAuth locally and just want to test the app, you can
/// specify a local IP address (if the device is connected to the same network
/// as the computer running FusionAuth) or even use ngrok to expose your
/// instance to the Internet temporarily.
const String FUSIONAUTH_DOMAIN = 'your-fusionauth-public-url';
const String FUSIONAUTH_SCHEME = 'https';
const String FUSIONAUTH_CLIENT_ID = 'e9fdb985-9173-4e01-9d73-ac2d60d1dc8e';
const String FUSIONAUTH_REDIRECT_URI =
'com.fusionauth.flutterdemo://login-callback';
const String FUSIONAUTH_LOGOUT_REDIRECT_URI =
'com.fusionauth.flutterdemo://logout-callback';
const String FUSIONAUTH_ISSUER = '$FUSIONAUTH_SCHEME://$FUSIONAUTH_DOMAIN';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
bool isBusy = false;
bool isLoggedIn = false;
String? errorMessage;
String? name;
String? picture;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'FusionAuth on Flutter ',
home: Scaffold(
appBar: AppBar(
title: const Text('FusionAuth on Flutter Demo'),
),
body: Center(
child: isBusy
? const CircularProgressIndicator()
: isLoggedIn
? Profile(logoutAction, name, picture)
: Login(loginAction, errorMessage),
),
),
);
}
Future<Map<String, dynamic>> getUserDetails(String accessToken) async {
final http.Response response = await http.get(
Uri.parse('$FUSIONAUTH_SCHEME://$FUSIONAUTH_DOMAIN/oauth2/userinfo'),
headers: <String, String>{'Authorization': 'Bearer $accessToken'},
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to get user details');
}
}
Future<void> loginAction() async {
setState(() {
isBusy = true;
errorMessage = '';
});
try {
final AuthorizationTokenResponse? result = await appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(
FUSIONAUTH_CLIENT_ID,
FUSIONAUTH_REDIRECT_URI,
issuer: FUSIONAUTH_ISSUER,
scopes: <String>['openid', 'offline_access'],
// promptValues: ['login']
),
);
if (result != null) {
// final Map<String, Object> idToken = parseIdToken(result.idToken);
final Map<String, dynamic> profile = await getUserDetails(result.accessToken!);
debugPrint('response: $profile');
await secureStorage.write(
key: 'refresh_token', value: result.refreshToken);
await secureStorage.write(
key: 'id_token', value: result.idToken);
var gravatar = Gravatar(profile['email']! as String);
var url = gravatar.imageUrl(
size: 100,
defaultImage: GravatarImage.retro,
rating: GravatarRating.pg,
fileExtension: true,
);
setState(() {
isBusy = false;
isLoggedIn = true;
name = profile['given_name']! as String;
picture = url;
});
}
} on Exception catch (e, s) {
debugPrint('login error: $e - stack: $s');
setState(() {
isBusy = false;
isLoggedIn = false;
errorMessage = e.toString();
});
}
}
Future<void> initAction() async {
final String? storedRefreshToken =
await secureStorage.read(key: 'refresh_token');
if (storedRefreshToken == null) {
return;
}
setState(() {
isBusy = true;
});
try {
final TokenResponse? response = await appAuth.token(TokenRequest(
FUSIONAUTH_CLIENT_ID,
FUSIONAUTH_REDIRECT_URI,
issuer: FUSIONAUTH_ISSUER,
refreshToken: storedRefreshToken,
));
if (response != null) {
// final Map<String, Object> idToken = parseIdToken(response.idToken);
final Map<String, dynamic> profile = await getUserDetails(response.accessToken!);
await secureStorage.write(key: 'refresh_token', value: response.refreshToken);
var gravatar = Gravatar(profile['email']! as String);
var url = gravatar.imageUrl(
size: 100,
defaultImage: GravatarImage.retro,
rating: GravatarRating.pg,
fileExtension: true,
);
setState(() {
isBusy = false;
isLoggedIn = true;
name = profile['given_name']! as String;
picture = url;
});
}
} on Exception catch (e, s) {
debugPrint('error on refresh token: $e - stack: $s');
await logoutAction();
}
}
Future<void> logoutAction() async {
final String? storedIdToken = await secureStorage.read(key: 'id_token');
if (storedIdToken == null) {
debugPrint(
'Could not retrieve id_token for actual logout. Deleting local cookies only...');
} else {
try {
await appAuth.endSession(EndSessionRequest(
idTokenHint: storedIdToken,
postLogoutRedirectUrl: FUSIONAUTH_LOGOUT_REDIRECT_URI,
issuer: FUSIONAUTH_ISSUER,
allowInsecureConnections: FUSIONAUTH_SCHEME != 'https'));
} catch (err) {
debugPrint('logout error: $err');
}
}
await secureStorage.deleteAll();
setState(() {
isLoggedIn = false;
isBusy = false;
});
}
}
class Login extends StatelessWidget {
final Future<void> Function() loginAction;
final String? loginError;
const Login(this.loginAction, this.loginError, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () async {
await loginAction();
},
child: const Text('Login'),
),
Text(loginError ?? ''),
],
);
}
}
class Profile extends StatelessWidget {
final Future<void> Function() logoutAction;
final String? name;
final String? picture;
const Profile(this.logoutAction, this.name, this.picture, {Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 200,
height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.orange, width: 4),
shape: BoxShape.circle,
image: (picture == null) ? null : DecorationImage(
fit: BoxFit.fill,
image: NetworkImage(picture!),
),
),
),
const SizedBox(height: 24),
Text('Name: $name'),
const SizedBox(height: 48),
ElevatedButton(
onPressed: () async {
await logoutAction();
},
child: const Text('Logout'),
),
],
);
}
}
In the beginning of the file, change the FUSIONAUTH_DOMAIN
constant to the public address for your instance (the same one you used when configuring it). If it is not running on HTTPS, you should also change FUSIONAUTH_SCHEME
to http
.
This code uses the system browser instead of an embedded webview due to security concerns. A webview is totally controlled by the native application displaying it, the current mobile best practices for OAuth require you to use the system browser, which is not under that control. From section 8.12 of that document:
This best current practice requires that native apps MUST NOT use embedded user-agents to perform authorization requests and allows that authorization endpoints MAY take steps to detect and block authorization requests in embedded user-agents.
Now, start up your emulators or real devices again.
flutter run -d all
After the application is loaded, click the "Login" button. Log in with the user account you created when setting up FusionAuth, and you’ll see the user picture and name next to a logout button.
You should see something similar to this demo:
Additional resources
Want to dive in further? Here are some additional resources for understanding auth in Flutter and mobile applications.
- Flutter AppAuth Plugin
- Auth in Flutter
- FusionAuth Dart Client Library
- Native app OAuth best practices
Feedback
How helpful was this page?
See a problem?
File an issue in our docs repo
Have a question or comment to share?
Visit the FusionAuth community forum.