New Westminster, BC, Canada

Flutter MFA with Azure Active Directory

Flutter MFA with Azure Active Directory

Azure Active Directory (Azure AD) is a cloud-based identity and access management (IAM) solution from Microsoft that provides a wide range of features for managing user identities and access to resources in the cloud and on-premises. The main reasons you might choose Azure AD over other cloud authentication solutions are:

  1. Integration with Microsoft Services: If you are already using Microsoft services such as Office 365, Azure, Dynamics 365, or Power Platform, then Azure AD provides seamless integration with these services, making it easy to manage user identities and access to these resources.
  2. Enterprise-Grade Security: Azure AD provides a range of security features such as multi-factor authentication, conditional access, identity protection, and threat intelligence that help to secure your organization’s resources from unauthorized access and data breaches.
  3. Hybrid Identity Management: Azure AD provides a hybrid identity model that allows you to manage both on-premises and cloud-based identities using a single solution. This makes it easier to manage identities and access across your organization, regardless of where the resources are located.
  4. Customizable and Flexible: Azure AD is highly customizable, allowing you to tailor the solution to meet the specific needs of your organization. You can use Azure AD to manage user access to custom applications, SaaS applications, and other cloud resources.

Overall, Azure AD provides a robust, scalable, and secure solution for managing user identities and access to cloud resources. If your organization is already using Microsoft services or needs a highly customizable and flexible IAM solution, then Azure AD may be a good fit.

How do Azure and Flutter get along?

As of now, neither Microsoft nor Google has provided official support for Azure AD for the Flutter framework. That means that there isn’t a plugin that either the former or the latter has stamped with their name and supports it officially. While we could speculate on the reasons why that is, I would prefer to simply show you how to make AAD work with Flutter quite nicely.

The best way to do that is with the help of a third-party plugin that has passed the test of time and proven itself as reliable, bug-free, and easy to use: the azure_ad_oauth. This plugin provides all the boilerplate we need for our authentication to work, while all we need to do is to call only a handful of methods.

1. Create App Registration

First things first: create an app registration in Azure Portal and acquire two secrets: tenant ID and client ID.

2. Add azure_ad_oauth to your project

The first thing we need to do is to run a command to add the plugin to your dependencies:

flutter pub add azure_ad_oauth

Now let’s add a file called authenticator.dart (or you can simply copy it from the example application of that plugin). The code in it will be as follows:

import 'package:azure_ad_oauth/azure_ad_oauth.dart';
import 'package:azure_ad_oauth/jwt.dart';
import 'package:flutter/foundation.dart';

class OAuth extends ChangeNotifier {
  /// Tenant ID of you Azure account
  static const String tenantId = ''; // TODO: set your tenant ID

  /// Client ID of you Azure application
  static const String clientId = ''; // TODO: set your client ID

  late final AzureADoAuth oAuth;
  late final Config config;
  Map<String, dynamic> map = {};
  bool loginInProgress = false;

  //singleton pattern
  static final OAuth _instance = OAuth._();

  OAuth._() {
    if (tenantId.isEmpty || clientId.isEmpty) {
      throw Exception('Please set tenantId and clientId');
    }
    String redirectUri;
    if (kIsWeb) {
      final currentUri = Uri.base;
      redirectUri = Uri(
        host: currentUri.host,
        scheme: currentUri.scheme,
        port: currentUri.port,
        path: '/authRedirect.html',
      ).toString();
    } else {
      redirectUri = 'msal$clientId://auth';
    }
    config = Config(
      tenantId: tenantId,
      clientId: clientId,
      scope: 'openid profile offline_access',
      responseType: kIsWeb ? 'id_token+token' : 'code',
      redirectUri: redirectUri,
      // the following sets the token refresh to 2 minutes
      //before the token expires.
      // This protects you from the token expiring during the API call.
      tokenRefreshAdvanceInSeconds: 120,
    );
    oAuth = AzureADoAuth(config);
  }

  static OAuth get instance => _instance;
  bool get isLoggedIn => oAuth.isLoggedIn;

  /// use this for all your API calls
  Future<String?> get token => oAuth.getIdToken();

  /// Get JWT data
  Future<Map<String, dynamic>> getJwtData() async {
    if (!oAuth.isLoggedIn) return {};
    return Jwt.parseJwt((await oAuth.getIdToken())!);
  }

  /// Login to Azure AD
  Future<void> login() async {
    loginInProgress = true;
    notifyListeners();
    try {
      await oAuth.login();
      map = await getJwtData();
    } on Exception {
      loginInProgress = false;
      notifyListeners();
      rethrow;
    }
    loginInProgress = false;
    notifyListeners();
  }

  /// Logout from Azure AD
  Future<void> logout() async {
    loginInProgress = true;
    notifyListeners();
    map = {};
    try {
      await oAuth.logout();
    } on Exception {
      loginInProgress = false;
      notifyListeners();
      rethrow;
    }

    loginInProgress = false;
    notifyListeners();
  }
}

The first two fields of that OAuth class are assigned the values we obtained in step one of this article. It’s up to the reader how to secure the secrets and avoid hardcoding them – we will not discuss this in this article.

The OAuth class uses a singleton pattern, which is very convenient and allows us to access the authenticator anywhere and at any point in our app.

Believe it or not – we are done and now we are ready to authenticate against our directory. In some cases, you may want to tweak the scope field of the config, but it should work as is.

3. Authenticate

If you’d like to try it – please try the example app provided with the plugin, but in a nutshell, here’s a simple example of the login page:

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        // !!! - - - - - - - - - - - - - - - - - - - - - - - -
        // On first call, we must provide the context
        // in order for the plugin to be able to display the
        // authentication view to the user!
        // !!! - - - - - - - - - - - - - - - - - - - - - - - -
        animation: OAuth.instance..config.context = context,
        builder: (context, _) {
          return Scaffold(
            appBar: AppBar(
              title: Text(widget.title),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  if (OAuth.instance.isLoggedIn &&
                      !OAuth.instance.loginInProgress)
                    Text(
                        'WELCOME\n ${OAuth.instance.map['name']}\n(${OAuth.instance.map['preferred_username']})',
                        textAlign: TextAlign.center),
                  const SizedBox(height: 20),
                  ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      fixedSize: const Size(200, 50),
                    ),
                    onPressed: OAuth.instance.loginInProgress
                        ? null
                        : () {
                            OAuth.instance.isLoggedIn
                                ? OAuth.instance
                                    .logout()
                                    .onError((error, stackTrace) {
                                    ScaffoldMessenger.of(context).showSnackBar(
                                        SnackBar(
                                            content: Text(error.toString())));
                                    setState(() {});
                                  })
                                : OAuth.instance
                                    .login()
                                    .onError((error, stackTrace) {
                                    ScaffoldMessenger.of(context).showSnackBar(
                                        SnackBar(
                                            content: Text(error.toString())));
                                    setState(() {});
                                  });
                          },
                    child: OAuth.instance.loginInProgress
                        ? const SizedBox(
                            width: 16,
                            height: 16,
                            child:
                                CircularProgressIndicator(color: Colors.white))
                        : Text(OAuth.instance.isLoggedIn ? 'Logout' : 'Login'),
                  ),
                ],
              ),
            ),
          );
        });
  }

4. Making Authenticated REST Calls

Now, the funny part is that you don’t even need a login page – you can simply make authenticated API calls by providing the following header in your request:

'Authorization: Bearer ${await OAuth.instance.token}'
or
'Authorization: Bearer ${await OAuth.instance.getToken()}'

Whenever you will try to get the token, the plugin will try its best to provide it. If the token is valid – you are good to go. If the token is invalid or expired – it will try and run the user through token refresh or full authentication. And if the authentication fails – it will throw an exception with the message “Access denied or authentication canceled”. As simple as that!

GET IN TOUCH

    X
    CONTACT US