Getting Azure AD authentication to work smoothly in Flutter apps

post-thumb

As a former Xamarin developer, I am used to work with AzureAD and the infamous Microsoft Authentication library (MSAL) as the authentication backbone for my apps. However, when me and my team switched to Flutter two years ago and started migrating apps for our customers, we faced several problems getting AzureAD to work together with Flutter apps in a smooth way.

Preface

So, here I want to share with you some of the major issues using AzureAD/MSAL in conjunction with Flutter and how we solved them. Since we use Remi Rousselet’s outstanding riverpod for state management, most of the code samples are based on riverpod, but all of this should be easily transferable to any other state management solution.

Also, I heavily recommend using gbwisx’ port of MSAL, which you can find here. It is very well documented and works smoothly together with both the system browser and Microsoft’s Authenticator app on iOS and Android. This article assumes that you have already set up MSAL following the plugin’s documentation on GitHub.

What this blog post is not about

This is neither a deep dive into the inner workings of AzureAD or MSAL, nor a riverpod tutorial. There are many well-curated resources out there that help you getting started, e.g.

What we are going to build

We want to tell a user to sign in whenever needed — and handle any changes to the authentication state in the UI. In this example, we will build something like this:

As long as no user is signed in, there is a bottom sheet indicating that a sign in is needed. No request is performed as long as nobody is signed in.

Once the user signs in, the authentication process begins (here, I did not have Microsoft’s Authenticator app installed, so the authentication was handled in a browser window by MSAL).

After authentication, the current user is shown and the bottom sheet is gone. Requests will automatically be authenticated from now on.

Set up a service handling the authentication logic

First of all, we need to enable our app to sign in and sign out users. This is quite straightforward. The _AuthenticationService below handles the authentication logic (signing in, signing out, getting a token, handling the Microsoft Authenticator logic, etc.) and there is merely any difference to the samples provided in msal_mobile’s documentation.

As the authentication logic used by msal_mobile lacks a bit of flexibility, we decided to go with a litte initialization hack that keeps track of the initialization state of the MSAL instance. When initializing, msal_mobile reads a configuration file that contains information about the app’s signature, the AzureAD instance to authenticate against and the integration of Microsoft’s Authenticator app.

So, we simply follow the basic principles when using MSAL here. Finally, we provide the service as a trackable dependency using riverpod.

final _authenticationServiceProvider = Provider((ref) => _AuthenticationService());

class _AuthenticationService {
  static const String _SCOPE = 'https://example.com/user_impersonation'; // Adjust to your AzureAD specs
  static const String _AUTHORITY = 'https://login.microsoftonline.com/aaaaaaa-bbbbbb-cccccc-ddddd'; // Adjust to your AzureAD specs

  bool _isInitialized = false;
  MsalMobile _msal;

  Future<MsalMobileAuthenticationResultPayload> signIn() async {
    await _initializeIfNeeded();

    if (!await _msal.getSignedIn()) {
      // If user is NOT signed in, do a full sign-in
      return await _signIn();
    } else {
      return await _acquireToken().then((result) {
        return result;
      }).catchError((error) {
        // Handle errors
      });
    }
  }

  Future<void> signOut() async {
    await _initializeIfNeeded();

    if (await getIsSignedIn()) {
      await _msal.signOut();
    }
  }

  Future<bool> getIsSignedIn() async {
    await _initializeIfNeeded();

    return await _msal.getSignedIn();
  }

  Future<MsalMobileAuthenticationResultPayload> _signIn() async {
    await _initializeIfNeeded();

    return await _msal.signIn(null, [_SCOPE]);
  }

  Future<MsalMobileAuthenticationResultPayload> _acquireToken() async {
    await _initializeIfNeeded();

    return await _msal.acquireToken([_SCOPE], _AUTHORITY);
  }

  Future<void> _initializeIfNeeded() async {
    if (_isInitialized) {
      return;
    }

    if (_msal == null) {
      _msal = await MsalMobile.create('assets/auth_config.json', _AUTHORITY);

      _isInitialized = true;
    }
  }
}

Notify listeners about the authentication state

So our authentication service can now handle the lower-level authentication logic — great! But, still the UI does not get notified about any changes at all.

  • What if the user signs out?
  • What is the current state? Is a user currently signed in? Can we make a request without running into 401 codes?
  • Should the app show a banner indicating that the user needs to sign in?

We need to keep track of the authentication state, which is exactly what riverpod is made for. Below, we provide a AuthenticationNorifier to our app that encapsulates the logic handled in our _AuthenticationService. Whenever the authentication state changes, each listener is being notified and can react accordingly. For the sake of simplicity, in this sample we only use a boolean flag indicating whether any user is signed in or nor. One could easily expand the states, e.g. by indicating that there is a user currently signed in (in MSAL this means that there is a last-known UserAccount), but there is no valid token available.

final authenticationProvider = ChangeNotifierProvider<AuthenticationNotifier>((ref) {
  final authenticationService = ref.read(_authenticationServiceProvider);

  return AuthenticationNotifier(authenticationService);
});

class AuthenticationNotifier extends ChangeNotifier {
  final _AuthenticationService _auth;

  AuthenticationNotifier(this._auth) {
    calculateSignInStatus();
  }

  bool _isSignedIn = true;
  bool get isSignedIn => _isSignedIn;

  void _setSignIn(bool newIsSignedIn) {
    if (_isSignedIn == newIsSignedIn) {
      return;
    }

    _isSignedIn = newIsSignedIn;
    notifyListeners();
  }

  Future<void> calculateSignInStatus() async {
    var isSignedIn = await _auth.getIsSignedIn();

    _setSignIn(isSignedIn);
  }

  Future<MsalMobileAuthenticationResultPayload> signIn() async {
    var result = await _auth.signIn();

    if (result != null) {
      _setSignIn(true);
      return result;
    }

    return null;
  }

  Future<void> signOut() async {
    await _auth.signOut();
    _setSignIn(false);
  }
}

Authenticating requests

This one had us badly scratching our heads at first. If we actually perform a request, how can we make sure that the request is authenticated correctly? Before we came across interceptors in Dio, we had pretty bad-shaped singleton implementation of our request handler, calling into our _AuthenticationService directly before a request was performed.

Even though the solution below looks quite easy, we needed some time to get to this, as we did non use provider references (ref) in the beginning.

final dioProvider = Provider.autoDispose<Dio>((ref) {
  final auth = ref.watch(authenticationProvider);

  final dio = Dio();

  dio.options.headers['Content-Type'] = 'application/json';
  dio.options.baseUrl = my-base-url;

  dio.interceptors.clear();
  dio.interceptors.add(
    InterceptorsWrapper(
      onRequest: (RequestOptions options) async {
        if (!await auth.isSignedIn) {
          return null; // Or handle otherwise
        }

        var res = await auth.signIn();

        options.headers['Authorization'] = 'Bearer ${res.accessToken}';
        return options;
      },
    ),
  );

  return dio;
});

One great thing about riverpod’s reference mechanism is that it allows listening to every state change happening in our _AuthenticationService easily.

The interceptor now authenticates each and every request and makes sure that there is no request being left un-authenticated (which would finally lead to 401 errors). For the sake of simplicity, I skipped the interceptor that handles any 401 error properly in the gist.

Showing the current authentication state ob the surface

So, how can we tell the user that he needs to sign in? As our authentication stat is now porperly handled and published to subscribers, we only need to listen to changes in the UI. For this example, we make use of hooks_riverpod in order to use the provided authenticationProvider. The scaffold shown below is state-aware to any changes happening to the authentication state. Here, we simply show a bottom sheet telling the user to sign in if needed:

class StateAwareScaffold extends HookWidget {
  final Widget body;

  const StateAwareScaffold({Key key, @required this.body}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final authenticationState = useProvider(authenticationProvider);

    return Scaffold(
      body: body,
      bottomSheet: authenticationState.isSignedIn ? Container(width: 0, height: 0) : SignInBottomSheet(),
    );
  }
}

Wrap up

And that’s it! We managed to create a wrapper around msal_mobile that notifies the UI about any changes to the authentication state in the app. Furthermore, we created an interceptor for Dio to make sure that every request is authenticated properly. Finally, we notified the user about a missing sign in.

Packages shown in this article

Reach out if you have questions

If you have questions on how to manage authentication state with MSAL and riverpod, just head over to my GitHub profile where you can find links to my social media.

Happy coding and thank you for coming around!

Der Objektkultur-Newsletter

Mit unserem Newsletter informieren wir Sie stets über die neuesten Blogbeiträge,
Webcasts und weiteren spannende Themen rund um die Digitalisierung.

Newsletter abonnieren