Home Automagic Tenant Config for Mobile Apps
Post
Cancel

Automagic Tenant Config for Mobile Apps

Watch: Video presentation of this topic

Contents

Background

With consumer apps (and some Enterprise apps), there’s usually only one config and one backend - the public facing instance. If you’re building an Enterprise mobile app, it’s far more likely you’ll need to support multiple client configurations.

For instance, your mobile app may need to connect to a different back end or tennant for each organisation that’s using your app. For example, let’s take a look at the difference between how a consumer app like WhatsApp works, and how a consumer or Enterprise app like Outlook works.

WhatsApp vs Outlook (Note: Oversimplified for demonstration purposes)

In this example, any user can install WhatsApp, and they’re up and running straight away. There’s one back end service that everyone connects too, and all other configuration is the same.

Compare this to Outlook, and in this example:

  • User A connects to an on-premises Exchange server
  • User B connects to a different on-premises Exchange server
  • User C connects to Exchange Online in Office 365

Each user needs a different set of configuration to get up and running with their email service. You may need to provide your user with all kinds of tenant-specific information, including:

  • The URL of the back end service
  • What identity provider (IDP) you are using (you may want to support on-premises Active Directory, Azure AD, Auth0, Okta, etc.)
  • The configuration for your IDP
  • If you use a mapping service, what maps provider you are using (e.g. Azure, Google, etc.)
  • What logging level your organisation wants to use across all devices

These are just some examples. There could be countless others depending on your unique needs.

With a web app, this is a simple problem to solve. You configure the instance or tenant using any of a number of established methods (DevOps pipeline variables, app configuration, etc.). Then all you need to do is give them the URL, and they can access the app with everything already configured for them. But the real trick is getting this configuration into your users’ hands and onto their mobile device.

You also need to do it in a way that is as easy for them as possible. Users have come to expect a level of automagic configuration in the software we build for them now; and it doesn’t matter how sophisticated your app is - if they have to manually enter all that configuration, they’re not going to be happy.

Let’s take a look at some ways we can get this config to your users.

Option A - Manual Configuration

One way to get tenant-specific config into your users’ mobile apps is manually. Provide them with the config, and get them to put it in.

Manual configuration entry form Manual Configuration Entry Form

In this example, the user is presented with a form which tells them to enter the configuration provided to them by their sysadmin (just like the old days with Outlook on desktop). Their sysadmin presumably sends them an email or directs them to an intranet resource where their config is documented.

Let’s take a look at the code for how a simple form like this works

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<StackLayout Margin="30"
             Padding="10"
             Spacing="10">
    <Image Source="medmanlogo"/>
    <Label HorizontalOptions="CenterAndExpand"
           HorizontalTextAlignment="Center"
           Text="Config"
           FontSize="Title"/>

    <Label HorizontalOptions="CenterAndExpand"
           HorizontalTextAlignment="Center"
           Text="Get these settings from your system administrator"/>

    <Entry Text="{Binding ApiBaseUri}"
           Placeholder="API URI"/>

    <Entry Text="{Binding IDP}"
           Placeholder="Identity Provider"/>

    <Entry Text="{Binding TenantId}"
           Placeholder="Tenant ID"/>

    <Entry Text="{Binding TenantName}"
           Placeholder="Tenant Name"/>

    <Entry Text="{Binding AppId}"
           Placeholder="App / Client ID"/>

    <Entry Text="{Binding SigninPolicy}"
           Placeholder="Signin Policy ID"/>

    <Label Text="Settings not valid"
           TextColor="DarkRed"
           HorizontalOptions="CenterAndExpand"
           HorizontalTextAlignment="Center"
           IsVisible="{Binding IsValid, Converter={StaticResource InverseBool}}"/>

    <Button Text="Save"
            BackgroundColor="{StaticResource Primary}"
            Command="{Binding SaveConfigCommand}"/>

</StackLayout>

The form with fields for the user to enter config items

We can see here that we’ve got a very simple entry form, where the user can enter all the configuration required to connect and log into their instance of MedMan (a dummy application used for this demo - link to repo with full code at the bottom). The fields of the form are bound to properties of the ViewModel. When the user taps the Save button a command in the ViewModel is called, which performs some simple validation then saves all the values to secure storage. The app then navigates to a login page, which consumes some of the values the user has entered here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public string IDP { get; set; }
public string ApiBaseUri { get; set; }
public string TenantName { get; set; }
public string TenantId { get; set; }
public string AppId { get; set; }
public string SigninPolicy { get; set; }

public ICommand SaveConfigCommand => new Command(async () => await SaveConfig());

public bool IsValid { get; set; } = true;

private async Task SaveConfig()
{
    IsValid = ValidateConfig(); // Method omitted for brevity

    OnPropertyChanged("IsValid");

    if (!IsValid)
        return;

    await SecureStorage.SetAsync(nameof(App.Constants.ApiBaseUri), ApiBaseUri);
    await SecureStorage.SetAsync(nameof(App.Constants.IDP), IDP);

    if(IDP == "B2C")
    {
        await SecureStorage.SetAsync(nameof(App.Constants.TenantId), TenantId);
        await SecureStorage.SetAsync(nameof(App.Constants.TenantName), TenantName);
        await SecureStorage.SetAsync(nameof(App.Constants.AppId), AppId);
        await SecureStorage.SetAsync(nameof(App.Constants.SigninPolicy), SigninPolicy);
    }

    await App.Constants.InitialiseSecrets();
    MessagingCenter.Send<object>(this, "ConstantsSaved");

    await Shell.Current.GoToAsync("//LoginPage");
}

ViewModel for the config form

There are some pros and cons to this approach. The major advantage is that, for a developer, it’s trivial to build, and there are virtually no ‘moving parts’, so it’s also reliable.

The downside, and it’s a big one, is that this is a terrible user experience.

There’s plenty of room for human error here - the user could enter this information incorrectly (especially likely on a small touchscreen keyboard), the information they receive could be wrong or prone to change. And it puts the responsibility onto them to do something that we should take care of for them.

Option B - Automagic Config with QR Codes

To make this a better experience for the user, the first step is to get rid of that entry form.

If they’ve already got access to the web app then they already have access to all of their tenant or instance config - even though they may not realise it. So one option is to transfer that config from the web app to their mobile device.

One way to do that is by encoding the configuration into a QR code and scanning that on the mobile device.

Automagic config with a QR code Automagic config with a QR code

In this approach, when the user opens their app, they are directed to find a QR code in the desktop app and scan it to retrieve the config. The desktop app (Angular in this example) grabs the config from the back end as a JSON string, then Base64 encodes that, and then displays the Base64 endoded string as a QR code.

The mobile app then reverses this process - it scans the code, Base64 decodes the string to a JSON string, then deserializes the JSON string to a config object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<StackLayout Margin="30"
             Padding="10"
             Spacing="10">
    <Image Source="medmanlogo"/>
    <Label HorizontalOptions="CenterAndExpand"
           HorizontalTextAlignment="Center"
           Text="{Binding QRIcon}"
           FontFamily="{StaticResource MaterialIcons}"
           FontSize="65"
           TextColor="Black"/>

    <Label HorizontalOptions="CenterAndExpand"
           HorizontalTextAlignment="Center"
           Text="Scan the QR Code from MedMan in your browser"/>

    <zxing:ZXingScannerView OnScanResult="ZXingScannerView_OnScanResult"
                            WidthRequest="400"
                            HeightRequest="400"/>

</StackLayout>

A QR scanner instead of a form

1
2
3
4
5
6
7
...
private async void ZXingScannerView_OnScanResult(ZXing.Result result)
{
    await ViewModel.SaveScannedConfig(result.Text);
}
...

On a successful scan result, the data is passed to a method in the ViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public async Task SaveScannedConfig(string config)
{
    string jsonConfig = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(config));

    var dto = JsonConvert.DeserializeObject<ConfigDto>(jsonConfig);

    AppId = dto.clientId;
    TenantId = dto.domain;
    TenantName = dto.tenantName;
    SigninPolicy = dto.signUpSignInPolicyId;
    ApiBaseUri = dto.apiBaseUri;
    IDP = dto.idp;

    await SaveConfig();
}

The data is decoded, and then saved exactly the same way as with a form

After that the process is the same as in Option A - the values are saved in Secure Storage and the user is redirected to the login page.

Neat, huh?

This is definitely an improved experience over Option A for the user. The advantage is that it doesn’t require manual form entry so reduces the risk of human error. It’s also cool and techy.

But it has some downsides too. One of these is that the user needs to be at their desk and logged in to the desktop app before they can use the mobile app. Another is reliance on the camera - some users may not have a device with a camera (unlikely as that is), or their camera may be damaged. Or they may not want to give your app permission to access the camera. Personally, I have had mixed results with the reliability of QR code scanning on Android (using the ZXing library); YMMV, but if this is an approach you like and want to adopt, it’s something that you can overcome so you shouldn’t consider it a barrier. But it’s worth noting.

The biggest downside, though, is that it still requires a manual step for the user to configure their device. It may be ‘cool’ and techy, but I like to ask myself - does it pass the Gregory Benford test?

The answer in this case is no. Not only does the user have to take steps specifically for configuring their app, but the inner workings are on display for them to see.

Using this QR code approach is certainly viable (in fact I have this in production use in an app I’ve worked on), but it can be improved.

Option C - Automagic Config from a URL

In the above example, the Angular application retrieves the config from the back end, before encoding it into a QR code to display to the user. So, why not cut out the middle-man, and just give the user the URL so their app can pick up the config directly?

Automagic config from a URL

In this case, the user enters a URL. This would presumably be provided to them by their sysadmin, or it could be displayed from their desktop app or could even simply be the URL of the desktop app. In this case, this replaces scanning the QR code - the app downloads the JSON directly from the URL, deserializes it to a config object, then saves the values to Secure Storage.

This approach solves two of the problems from the QR code option - no reliance on camera, and the user doesn’t have to be at their desk. But it reintroduces the problem of manual entry and risk of human error, and it still doesn’t pass the Gregory Benford test.

Still, this is a good option to include in your app as a fallback (more on this at the end).

Option D - Automagic Config from Email

If your app requires your users to log in, it’s almost certain that they will be using their email address as the username. Seeing as they will be entering their email address anyway, why not use it to get their config for them automagically?

This option definitely passes the Gregory Benford test. Yes, it requires a manual step (entering an email address), but the user expects to do this anyway as part of the login process. The user doesn’t see this as a config step, just part of their normal login, and they have no idea that there is any ‘magic’ going on behind the scenes to configure their app for them. Also, while this is still a manual option (like filling in a form or entering a URL), the email address is something the user knows and uses all the time. So the risk of human error is lower, and even if the user makes a mistake, they can spot and correct that themselves easily.

Using a Config Broker

To pull off this config sleight-of-hand, let’s assume that any user with the same SMTP domain (the bit after the ‘@’ in your email address) will be part of the same tenant, and will therefore need the same config; just like with Microsoft Exchange or Exchange Online/Office365. One way you can do this is with a config broker service, that might look something like this.

Automagic config with a config broker

Let’s break down what’s happening here:

  1. The user enters their email address
  2. The app extracts the SMTP domain from their email address and sends it off to a config broker
  3. The config broker (in this case an Azure function) looks up their SMTP domain in a database (CosmosDB in this example) and returns the config associated with that domain
  4. The config is sent back to the app
  5. The app now knows what IDP it should use, the config for that IDP, where to find it’s back-end API, etc.

This is a viable approach and can work well. But there are some problems. Firstly, it’s a whole bunch of other services and resources that you have to maintain. This can be especially problematic if your clients host and manage their tenants themselves (rather than consuming a SaaS offering from you). As the app vendor, you still need to maintain this database, and are dependent on your clients giving you accurate and up to date information, which is the second problem.

Using DNS

Rather than reinventing the wheel, a far better approach is to replicate what Microsoft have done with Outlook.

The Outlook mobile app just asks users for their email address

How does Microsoft pull off this trick? The answer is surprisingly simple - with DNS records.

The administrator of any domain that an Outlook client is expected to connect to, whether on desktop or mobile, creates an autodiscover record for that domain. So, if I want to set up Outlook to use my email address (matt@goforgoldman.com), I just enter this into Outlook. Outlook then extracts the SMTP domain (goforgoldman.com) and looks up an autodiscover record for that domain - autodiscover.goforgoldman.com.

Outlook can now automatically configure itself just from your email address

This is a tried and tested approach that has been working for a long time. If we want to replicate this, we can do so easily. We can’t use autodiscover - that’s already taken by Outlook - but we can create a unique discovery record for our app that any domain administrator can easily create. In the case of our sample app - MedMan - we create a record called discovermedman.

The final version of our app just asks for the user’s email address

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<StackLayout Margin="30"
             Padding="10"
             Spacing="10">
    <Image Source="medmanlogo"/>
    <Label HorizontalOptions="CenterAndExpand"
           HorizontalTextAlignment="Center"
           Text="Config"
           FontSize="Title"/>

    <Label HorizontalOptions="CenterAndExpand"
           HorizontalTextAlignment="Center"
           Text="Enter your email address"/>

    <Entry Text="{Binding UserEmail}"
           Placeholder="Email"/>

    <Label Text="{Binding ValidationMessage}"
           TextColor="DarkRed"
           HorizontalOptions="CenterAndExpand"
           HorizontalTextAlignment="Center"
           IsVisible="{Binding IsValid, Converter={StaticResource InverseBool}}"/>

    <Button Text="Save"
            BackgroundColor="{StaticResource Primary}"
            Command="{Binding SaveConfigCommand}"/>

</StackLayout>

The email form now just has the one entry field

Once we have the user’s email address, we can extract the SMTP domain, and look for our config at discovermedman.{smtpdomain}.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// Get the domain from the email address
var domain = GetDomainFromEmail(UserEmail);

Uri configUri;

// Verify that the URL is valid
IsValid = Uri.TryCreate($"https://discovermedman.{domain}/api/config", UriKind.Absolute, out configUri);

OnPropertyChanged("IsValid");
OnPropertyChanged("ValidationMessage");

if (!IsValid)
{
    ValidationMessage = "Not a valid URL. Please try again.";
    return;
}


// Get the config from the discover endpoint
using (HttpClient client = new HttpClient())
{
    var configResult = await client.GetAsync(configUri);

    if(!configResult.IsSuccessStatusCode)
    {
        IsValid = false;
        ValidationMessage = "Could not retrieve config";
        OnPropertyChanged("IsValid");
        OnPropertyChanged("ValidationMessage");
        return;
    }

    configString = await configResult.Content.ReadAsStringAsync();
}

//validate config

try
{
    var dto = JsonConvert.DeserializeObject<ConfigDto>(configString);

    AppId = dto.clientId;
    TenantId = dto.domain;
    TenantName = dto.tenantName;
    SigninPolicy = dto.signUpSignInPolicyId;
    ApiBaseUri = dto.apiBaseUri;
    IDP = dto.idp;
}
catch (Exception)
{
    IsValid = false;
    ValidationMessage = "Not a valid MedMan URL";
    OnPropertyChanged("IsValid");
    OnPropertyChanged("ValidationMessage");
    return;
}

IsValid = ValidateConfig();

if (!IsValid)
{
    ValidationMessage = "Config from MedMan is invalid";
    OnPropertyChanged("IsValid");
    OnPropertyChanged("ValidationMessage");
    return;
}

// if got this far, config was retrieved successfully and is good

// save the email to an App constant for use in the login page

App.Constants.UserEmail = UserEmail;

We get the user’s domain from their email address, then use it to fetch the config. Then save as normal.

All required config can now be retrieved using an autodiscover record

This is a much better approach than using a config broker, because the responsibility for maintaining this is with the administrator of the SMTP domain for the tenant. It’s true that some may not do it or do it wrong, but in this case it affects one subset of your users, rather than your whole user base.

One last trick…

This is great so far. The user has given us their email address so we can get the config, but we also need their email address for the login. To maintain the illusion, they shouldn’t have to enter it again. Instead, it should automagically flow through to the login screen.

Animation showing email address captured and passed through to OAuth login page

This is achieved through use of the ‘Login Hint’ parameter. This parameter is not strictly defined as part of the OAuth standard, but nearly all OAuth compliant identity providers - and certainly all the major ones - support it. In this example we’re using Azure AD B2C as the IDP and the Microsoft Authentication Library (MSAL) in the client to authenticate against it.

MSAL defines Login Hint as an extension for B2C clients, allowing you to pass a value to a named parameter. But you can just pass it as part of the URL query string too (for any provider that supports it).

In our demo app, when the user enters their email, we don’t just use it to look up the config, we also store it in a global state parameter (in this case in a static Constants class). Then when we instantiate our authentication client, we pass in the username (the user’s email) as a Login Hint paramter.

1
2
3
4
5
6
7
8
9
10
11
12
13
private async void OnLoginClicked(object obj)
{
    var result = await App.AuthenticationClient
        .AcquireTokenInteractive(App.Constants.Scopes)
        .WithParentActivityOrWindow(App.UIParent)
        .WithUseEmbeddedWebView(true)
        .WithLoginHint(App.Constants.UserEmail) // The user's email address is re-used here
        .ExecuteAsync();

    App.Constants.BearerToken = result.AccessToken;

    await Shell.Current.GoToAsync($"//{nameof(PatientsPage)}");
}

When we saved the config, we stored the user’s email address in a Constant, and re-use it here

This lets us use the email address we’ve already acquired from the user without them having to enter it again.

Magic! 🪄

Final thoughts

TL;DR: the best way to automatically configure your enterprise mobile app for your users is with their email address. They’re already expecting to use it to log in, so once you have it, using it to get their configuration as well is an invisible, magical experience for them.

In these examples I showed different methods for configuring a mobile app individually. In the real world, you want to offer fallback options. For example, if the SMTP domain administrator hasn’t created your discover record, the user will need another approach. In this case you could offer them the option of scanning a QR code (you might note in the Outlook screenshot that is offered as an alternative option.)

If their camera doesn’t work, or they don’t want to use it or don’t have access to the QR code, you should offer them the option of entering a config URL. And finally, as a last resort in case everything else has failed, you should offer them a manual config form.

When building software, ask yourself whether it passes the Gregory Benford test. If it does - great! But if not, spare a thought for how you can make it seem more magical.

Resources

A GitHub repository featuring all the code for this sample can be found here: https://github.com/MattGoldmanSSW/automagic

The different options shown here for the mobile app (Xamarin.Forms) are on different branches, and you will also find the back-end (.NET Core) and the web app (Angular) here too.

This post is licensed under CC BY 4.0 by the author.

HTTP Status Codes in your APIs

Sending Email via Office365 Exchange Online with Fluent Email