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:

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:

Flutter login screen Auth0 prompt page in a Flutter app
Auth0 Universal Login Page in a Flutter app Flutter profile screen


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 {
  @override
  _MyAppState createState() => _MyAppState();
}

/// -----------------------------------
///              App State            
/// -----------------------------------

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: '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.

  • flutter_appauth: A well-maintained wrapper package around AppAuth for Flutter developed by the dexterx.dev team. AppAuth authenticates and authorizes users and supports the PKCE extension.

  • flutter_secure_storage: A library to securely persist data locally.

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.

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

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);

  @override
  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);

  @override
  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.

Auth0 Create application screen for Flutter app

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).

Auth0 connections for Flutter apps

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:

Auth0 callback settings for Flutter apps

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.

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.

Auth0 settings for Flutter apps

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;

  @override
  Widget build(BuildContext context) {...}

  Map<String, dynamic> parseIdToken(String idToken) {...}

  Future<Map> getUserDetails(String accessToken) async {...}

  Future<void> loginAction() async {...}

  void logoutAction() async {...}

  @override
  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.

Retrieve 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:

  1. accessToken: an OAuth 2.0 artifact that allows the application to call secure APIs on behalf of the user.

  2. idToken: user profile information in JWT format.

  3. refreshToken: a token to obtain a new accessToken and idToken.

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:

  @override
  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:

  @override
  Widget 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),
        ),
      ),
    );
  }

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:

Flutter Android login screen

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:

Flutter iOS consent prompt

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:

Flutter Android Universal Login screen

On this screen, either enter your credentials or click "Sign in with Google". Either way, once you successfully log in, the profile screen renders:

Flutter Android profile screen

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.