次の方法で共有


Web-based login on WPF projects for Azure Mobile Services

In almost all supported client platforms for Azure Mobile Services, there are two flavors of the login operation. The first one the client talks to an SDK specific to the login provider (i.e., the Live Connect SDK for Windows Store apps, the Facebook SDK for iOS, and so on) and then uses a token which they got from that provider to login to their mobile service – that's called the client-side authentication flow, since all the authentication action happens without interaction with the mobile service. On the other alternative, called server-side authentication flow, the client opens a web browser window (or control) which talks, via the mobile service runtime, to the provider web login interface, and after a series of redirects (which I depicted in a previous post) the client gets the authentication token from the service which will be used in subsequent (authenticated) calls. There’s one platform, however, which doesn’t have this support - “full” .NET 4.5 (i.e., the “desktop” version of the framework).

That platform is lacking that functionality because there are cases where it cannot display a web interface where the user can enter their credentials. For example, it can be used in a backend service (in which really there’s no user interface to interact with, like the scenario I showed in the post about a MVC app accessing authenticated tables in an Azure Mobile Service). It can also be a console application, in which there’s no “native” way to display a web page. Even if we could come up with a way to display a login page (such as in a popup window), what kind of window to use? If we go with WPF, it wouldn’t look natural in a WinForms app, and vice-versa.

We can, however, solve this problem if we constrain the platform to one specific which supports user interface elements. In this post, I’ll show how this can be done for a WPF project. Notice that my UI skills are really, really poor, so if you plan on using it on a “real” app, I’d strongly recommend you to refine the interface. To write the code for this project, I took as a starting point the actual code for the login page from the client SDK (I used the Windows Phone as an example) – it’s good to have all the client code publicly available.

The server-side authentication flow

Borrowing a picture I used in the post about authentication, this is what happens in the server-side authentication flow, where the client shows what the browser control in each specific platform does.

3113_ServerSideAuthFlow_1225ED52

What we then need to do is to have a web browser navigate to //mobileservicename.azure-mobile.net/login/<provider>, and monitor its redirects until it reaches /login/done. Once that’s reached, we can then parse the token and create the MobileServiceUser which will be set in the client.

The library

The library will have one extension method on the MobileServiceClient method, which will display our own login page (as a modal dialog / popup). When the login finishes successfully (if it fails it will throw an exception), we then parse the token returned by the login page, create the MobileServiceUser object, set it to the client, and return the user (using the same method signature as in the other platforms).

  1. public async static Task<MobileServiceUser> LoginAsync(this MobileServiceClient client, MobileServiceAuthenticationProvider provider)
  2. {
  3.     Uri startUri = new Uri(client.ApplicationUri, "login/" + provider.ToString().ToLowerInvariant());
  4.     Uri endUri = new Uri(client.ApplicationUri, "login/done");
  5.     LoginPage loginPage = new LoginPage(startUri, endUri);
  6.     string token = await loginPage.Display();
  7.     JObject tokenObj = JObject.Parse(token.Replace("%2C", ","));
  8.     var userId = tokenObj["user"]["userId"].ToObject<string>();
  9.     var authToken = tokenObj["authenticationToken"].ToObject<string>();
  10.     var result = new MobileServiceUser(userId);
  11.     result.MobileServiceAuthenticationToken = authToken;
  12.     client.CurrentUser = result;
  13.     return result;
  14. }

The login page is shown below. To mimic the login page shown at Windows Store apps, I’ll have a page with a header, a web view and a footer with a cancel button:

LoginPageDesigner

To keep the UI part simple, I’m using a simple grid:

  1. <Grid Name="grdRootPanel">
  2.     <Grid.RowDefinitions>
  3.         <RowDefinition Height="80"/>
  4.         <RowDefinition Height="*"/>
  5.         <RowDefinition Height="80"/>
  6.     </Grid.RowDefinitions>
  7.     <TextBlock Text="Connecting to a service..." VerticalAlignment="Center" HorizontalAlignment="Center"
  8.                FontSize="30" Foreground="Gray" FontWeight="Bold"/>
  9.     <Button Name="btnCancel" Grid.Row="2" Content="Cancel" HorizontalAlignment="Left" VerticalAlignment="Stretch"
  10.             Margin="10" FontSize="25" Width="100" Click="btnCancel_Click" />
  11.     <ProgressBar Name="progress" IsIndeterminate="True" Visibility="Collapsed" Grid.Row="1" />
  12.     <WebBrowser Name="webControl" Grid.Row="1" Visibility="Collapsed" />
  13. </Grid>

In the LoginPage.xaml.cs, the Display method (called by the extension method shown above) creates a Popup window, adds the login page as the child and shows it; when the popup is closed, it will either throw an exception if the login was cancelled, or return the stored token if successful.

  1. public Task<string> Display()
  2. {
  3.     Popup popup = new Popup();
  4.     popup.Child = this;
  5.     popup.PlacementRectangle = new Rect(new Size(SystemParameters.FullPrimaryScreenWidth, SystemParameters.FullPrimaryScreenHeight));
  6.     popup.Placement = PlacementMode.Center;
  7.     TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
  8.     popup.IsOpen = true;
  9.     popup.Closed += (snd, ea) =>
  10.     {
  11.          if (this.loginCancelled)
  12.         {
  13.             tcs.SetException(new InvalidOperationException("Login cancelled"));
  14.         }
  15.         else
  16.         {
  17.             tcs.SetResult(this.loginToken);
  18.         }
  19.     };
  20.  
  21.     return tcs.Task;
  22. }

The navigation starts at the constructor of the login page (it could be moved elsewhere, but since it’s primarily used by the extension method itself, it can start navigating to the authentication page as soon as possible).

  1. public LoginPage(Uri startUri, Uri endUri)
  2. {
  3.     InitializeComponent();
  4.  
  5.     this.startUri = startUri;
  6.     this.endUri = endUri;
  7.  
  8.     var bounds = Application.Current.MainWindow.RenderSize;
  9.     // TODO: check if those values work well for all providers
  10.     this.grdRootPanel.Width = Math.Max(bounds.Width, 640);
  11.     this.grdRootPanel.Height = Math.Max(bounds.Height, 480);
  12.  
  13.     this.webControl.LoadCompleted += webControl_LoadCompleted;
  14.     this.webControl.Navigating += webControl_Navigating;
  15.     this.webControl.Navigate(this.startUri);
  16. }

When the Navigating event of the web control is called, we can then check if the URI which the control is navigating to is the “final” URI which the authentication flow expects. If it’s the case, then we extract the token value, storing it in the object, and close the popup (which will signal the task on the Display method to be completed).

  1. void webControl_Navigating(object sender, NavigatingCancelEventArgs e)
  2. {
  3.     if (e.Uri.Equals(this.endUri))
  4.     {
  5.         string uri = e.Uri.ToString();
  6.         int tokenIndex = uri.IndexOf("#token=");
  7.         if (tokenIndex >= 0)
  8.         {
  9.             this.loginToken = uri.Substring(tokenIndex + "#token=".Length);
  10.         }
  11.         else
  12.         {
  13.             // TODO: better error handling
  14.             this.loginCancelled = true;
  15.         }
  16.  
  17.         ((Popup)this.Parent).IsOpen = false;
  18.     }
  19. }

That’s about it. There is some other code in the library (handling cancel, for example), but I’ll leave it out of this post for simplicity sake (you can still see the full code in the link at the bottom of this post).

Testing the library

To test the library, I’ll create a WPF project, with a button an a text area to write some debug information:

TestWindow

And on the click handler, we can invoke the LoginAsync; to test it out I’m also invoking an API which I set with user permissions, to make sure that the token which we got is actually good.

  1. private async void btnStart_Click(object sender, RoutedEventArgs e)
  2. {
  3.     try
  4.     {
  5.         var user = await MobileService.LoginAsync(MobileServiceAuthenticationProvider.Facebook);
  6.         AddToDebug("User: {0}", user.UserId);
  7.         var apiResult = await MobileService.InvokeApiAsync("user");
  8.         AddToDebug("API result: {0}", apiResult);
  9.     }
  10.     catch (Exception ex)
  11.     {
  12.         AddToDebug("Error: {0}", ex);
  13.     }
  14. }

That’s it. The full code for this post can be found in the GitHub repository at https://github.com/carlosfigueira/blogsamples/tree/master/AzureMobileServices/AzureMobile.AuthExtensions. As usual, please let us know (either via comments in this blog or in our forums) if you have any issues, comments or suggestions.

Comments

  • Anonymous
    October 29, 2013
    For some reason the var result = new MobileServiceUser(userId); is returning null , Do you have any idea behind this issue ?
  • Anonymous
    October 29, 2013
    This should never happen. A "new" call in C# will always return a non-null object, or throw an exception. If this is not happening, then it's a compiler / C# runtime bug. Please make sure that this is the case (for example, after the call do something along the lines of <<Console.WriteLine("Is it really null? {0}", result == null);>> - or use MessageBox.Show if Console.WriteLine doesn't show you anything...
  • Anonymous
    October 30, 2013
    Thanks for  prompt reply. Let me troubleshoot further. I am able to get successfully get token but its not getting the profile data. Thanks.
  • Anonymous
    October 30, 2013
    The comment has been removed
  • Anonymous
    November 05, 2013
    The comment has been removed
  • Anonymous
    November 05, 2013
    Further digging shows the issue is im not getting #token at the end of the uri.Substring... no idea why, Im wondering if i have to configure Azure somehow?
  • Anonymous
    November 06, 2013
    Do you have everything set up for identity on the server side? Try following the steps at blogs.msdn.com/.../troubleshooting-authentication-issues-in-azure-mobile-services.aspx to see if they're fine at the server side before trying to use it from the client.
  • Anonymous
    November 06, 2013
    Hi Carlos, thanks for your reply, ill check out the link... but if i enter my url/login/google for example into a web browser im receiving the full response including the token which make me think Azure is set up correctly?
  • Anonymous
    November 06, 2013
    The comment has been removed
  • Anonymous
    November 27, 2013
    @Sam, if you look at the requests being sent over a network capture tool (such as Fiddler), do you see the #done fragment in the last redirect during the login (via the browser)?
  • Anonymous
    January 30, 2014
    Super helpful post Carlos. Thank you!Saved me a lot of time getting Google OAuth to work in my WPF app.
  • Anonymous
    September 17, 2014
    IE keeps prompting me to download a json file when i use this code.