Nick's .NET Travels

Continually looking for the yellow brick road so I can catch me a wizard....

Adding Azure Active Directory Authentication to Windows Phone 8.0 Application with Xamarin.Forms

In the previous post I covered addition authentication to the Windows platform applications but I explicitly excluded the Windows Phone 8.0 project that’s part of the XForms set of projects. This was because the ADAL doesn’t currently support Windows Phone 8.0. In this post we’ll add a custom implementation which will authenticate the user using a WebBrowser control within the Windows Phone 8.0 application. We’ll start by adding a reference to the Windows Phone 8.0 project to the Shared.Client project – this will cause build errors as there is no reference to ADAL so the classes which are referenced in this shared code don’t exist. I’ll make some small amendments using the SILVERLIGHT compilation attribute (note that this is only defined for the WP8.0 project):

using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
#if !SILVERLIGHT
using Microsoft.IdentityModel.Clients.ActiveDirectory;
#else
using RealEstateInspector.XForms.WinPhone;
#endif
using RealEstateInspector.Core;

namespace RealEstateInspector.Shared.Client
{
    public static class AuthenticationHelper
    {

        public static async Task<string> Authenticate()
        {
            try
            {
                var authContext = new AuthenticationContext(Constants.ADAuthority);
#if !SILVERLIGHT
                if (authContext.TokenCache.ReadItems().Count() > 0)
                    authContext = new AuthenticationContext(authContext.TokenCache.ReadItems().First().Authority);
#endif

                var authResult =
                    await
                        authContext.AcquireTokenAsync(Constants.MobileServiceAppIdUri,
                        Constants.ADNativeClientApplicationClientId,
                        new Uri(Constants.ADRedirectUri),
#if WINDOWS_PHONE_APP || SILVERLIGHT
                        new AuthorizationParameters()
#else
                            new AuthorizationParameters(PromptBehavior.Auto, false)
#endif
                        );
                Debug.WriteLine(authResult != null);

                return authResult.AccessToken;

            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
                return null;
            }
        }
    }
}

The next thing to do is to provide an implementation of the AuthenticationContext class and it’s associated classes. These are added to the Windows Phone 8.0 project:

public class AuthenticationResult
{
    [JsonProperty("access_token")]
    public string AccessToken { get; set; }
}

public class AuthorizationParameters
{
}

public class AuthenticationContext
{
    public string Authority { get; set; }
    public string Resource { get; set; }
    public string ClientId { get; set; }
    public Uri RedirectUri { get; set; }

    public AuthenticationContext(string authority)
    {
        Authority = authority;
    }

    public async Task<AuthenticationResult> AcquireTokenAsync(
        string resource,
        string clientId,
        Uri redirectUri,
        AuthorizationParameters parameters)
    {
        Resource = resource;
        ClientId = clientId;
        RedirectUri = redirectUri;
        var code = await Authenticate();

        var http = new HttpClient();

        var tokenUrl = string.Format("{0}/oauth2/token",Authority);
        var formData = new Dictionary<string, string>
        {
            {"grant_type","authorization_code"},
            {"client_id",ClientId},
            {"code",code},
            {"resource",Resource},
            {"redirect_uri",RedirectUri.OriginalString}
        };

        var content = new FormUrlEncodedContent(formData);
        var data = await http.PostAsync(new Uri(tokenUrl), content);
        var result = JsonConvert.DeserializeObject<AuthenticationResult>(await data.Content.ReadAsStringAsync());
        return result;
    }

    private ManualResetEvent authenticateWaiter = new ManualResetEvent(false);
    private string AccessToken { get; set; }
    public async Task<string> Authenticate()
    {
        authenticateWaiter.Reset();
        var authUrlTemplate =
            "{0}/oauth2/authorize?response_type=code&client_id={1}&redirect_uri={2}";
        var authUrl = string.Format(authUrlTemplate,
            Authority,
            ClientId,
            Uri.EscapeDataString(RedirectUri.OriginalString)
            );

        var page = (Application.Current.RootVisual as Frame).Content as Page;
        var firstChild = page.Content;
        if (!(firstChild is Grid))
        {
            page.Content = null;
            var gd = new Grid();
            gd.Children.Add(firstChild);
            page.Content = gd;
            firstChild = gd;
        }

        var mainGrid = firstChild as Grid;
        var browser = new WebBrowser
        {
            IsScriptEnabled = true
        };
        browser.Navigating += BrowserNavigating;
        Grid.SetRowSpan(browser, (mainGrid.RowDefinitions != null && mainGrid.RowDefinitions.Count > 0) ? mainGrid.RowDefinitions.Count : 1);
        Grid.SetColumnSpan(browser, (mainGrid.ColumnDefinitions != null && mainGrid.ColumnDefinitions.Count > 0) ? mainGrid.ColumnDefinitions.Count : 1);
        mainGrid.Children.Add(browser);
        browser.Navigate(new Uri(authUrl));

        await Task.Run(() => authenticateWaiter.WaitOne());
        return AccessToken;
    }

    private void BrowserNavigating(object sender, NavigatingEventArgs e)
    {
        if (e.Uri.OriginalString.ToLower().StartsWith(RedirectUri.OriginalString.ToLower()))
        {
            try
            {
                var query = e.Uri.OriginalString.Substring(e.Uri.OriginalString.IndexOf('?') + 1);
                var code = (from pair in query.Split('&')
                            let bits = pair.Split('=')
                            where bits.Length == 2
                                    && bits[0] == "code"
                            select bits[1]).FirstOrDefault();
                AccessToken = code;
            }
            catch (Exception ex)
            {
                AccessToken = null;
            }
            finally
            {
                authenticateWaiter.Set();
                var browser = sender as WebBrowser;
                browser.Navigating -= BrowserNavigating;
                (browser.Parent as Grid).Children.Remove(browser);
            }
        }
    }
}

Reading this code you’ll see that I’m manually inserting and removing a WebBrowser control into the current page of the WP8.0 application. This is then used to authenticate the user and return the

authorization code which is then used to make the POST to request the access token (basically following what I did in my previous post when I walked this through manually in the browser/Fiddler).

I’ve also updated the XAML in my XForms MainPage:

<Page
    x:Class="RealEstateInspector.MainPage"
    xmlns="
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.Resources>
            <DataTemplate
                x:Key="PropertyItemTemplate">
                <TextBlock
                    Text="{Binding Address}"
                    FontSize="30"
                    Foreground="WhiteSmoke"/>
            </DataTemplate>
        </Grid.Resources>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition />
        </Grid.RowDefinitions>
        <Button Content="Authenticate" Click="AuthenticateClick"/>
        <ListView
            ItemTemplate="{StaticResource PropertyItemTemplate}"
            Grid.Row="1"
            ItemsSource="{Binding Properties}"/>
    </Grid>
</Page>

As well as the code behind to handle the button click:

public static event EventHandler AuthenticateRequested;

public void AuthenticateClick(object sender, EventArgs e)
{
    if (AuthenticateRequested != null)
    {
        AuthenticateRequested(this, EventArgs.Empty);
    }
}

Lastly in the MainPage of my Windows Phone 8.0 application I’ve wired up a listener for the AuthenticateRequested event to make sure I can trigger the authentication process:

public MainPage()
{
    InitializeComponent();
    SupportedOrientations = SupportedPageOrientation.PortraitOrLandscape;

    global::Xamarin.Forms.Forms.Init();
    RealEstateInspector.XForms.MainPage.AuthenticateRequested += Authenticate;
    LoadApplication(new RealEstateInspector.XForms.App());
}

public async void Authenticate(object sender, EventArgs e)
{
    var page = sender as RealEstateInspector.XForms.MainPage;
    var token = await AuthenticationHelper.Authenticate();
    Debug.WriteLine(token);
    (page.BindingContext as MainViewModel).LoadPropertyData(token);
}

Pingbacks and trackbacks (2)+

Comments are closed