Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to enable App Service Mobile App SSO for UWP

I am building a Universal Windows Platform (UWP) app that uses the Azure App Service Mobile App backend as well as the user's OneDrive account. I have 2 requirements for authentication:

  1. If the user is logged in to their UWP device with a Microsoft account (e.g. Windows 10) then I don't want them to be presented with a login prompt (i.e. Single Sign On, re-using their Microsoft account credentials).
  2. I want to have a single authentication event across Azure & OneDrive, i.e. the user authorises once and I re-use that token for both services.

I did this in Windows Phone 8 with an Azure Mobile Service by logging in with the Live SDK and then passing the returned token to the MobileServiceClient.LoginAsync() method, however I can't get this to work in UWP with an Azure Mobile App. When I call that same method I receive a 401 Unauthorised response.

  • I have associated my UWP app with the store and set up the application at the Microsoft Account Developer Centre, including adding the redirect URI from the Azure Mobile App.
  • I have set up the Azure App Service Mobile App, including adding the Client ID & Secret from the Microsoft Account Developer Centre.
  • I have tried numerous ways to retrieve the token, including the OnlineIdAuthenticator, WebAuthenticationCoreManager and WebAuthenticationBroker. None has worked so far.

I currently use the following code in a class LiveAuthenticationService to retrieve an access token:

public async Task<bool> LoginAsync()
{
    AccessToken = null;
    bool success = false;
    OnlineIdAuthenticator onlineIdAuthenticator = new OnlineIdAuthenticator();
    EventWaitHandle waithandle = new ManualResetEvent(false);

    OnlineIdServiceTicketRequest serviceTicketRequest = new OnlineIdServiceTicketRequest(scopes, "DELEGATION");
    UserIdentity result = await onlineIdAuthenticator.AuthenticateUserAsync(serviceTicketRequest);
    if (!string.IsNullOrWhiteSpace(result?.Tickets[0]?.Value))
    {
        currentUserId = result.SafeCustomerId;
        AccessToken = result.Tickets[0].Value;
        success = true;
        waithandle.Set();
    }
    else
    {
        await logger.LogErrorAsync("Error signing in to Microsoft Live",
                                    new Dictionary<string, string> { { "errorCode", result?.Tickets[0]?.ErrorCode.ToString() } });
    }
    waithandle.WaitOne(10000);  //10 second timeout

    return success;
}

And then this to attempt to login to my Azure Mobile App with that token, which uses LiveAuthenticationService from above:

private async Task RefreshUserIdAndAccessToken()
{
    try
    {
        var tcs = new TaskCompletionSource<MobileServiceUser>();

        var authService = new LiveAuthenticationService();
        await UiDispatcher.RunAsync(CoreDispatcherPriority.Normal,
                                    async () =>
                                    {
                                        try
                                        {
                                            await authService.LoginAsync();
                                            var jsonAuthenticationToken = JObject.Parse(@"{""authenticationToken"": """ + authService.AccessToken + @"""}");
                                            tcs.SetResult(await mobileService.LoginAsync(MobileServiceAuthenticationProvider.MicrosoftAccount, jsonAuthenticationToken));
                                        }
                                        catch (Exception ex)
                                        {
                                            tcs.SetException(ex);
                                        }
                                    });

        var user = await tcs.Task;
        currentUserId = user.UserId;
        AccessToken = user.MobileServiceAuthenticationToken;
    }
    catch (Exception ex)
    {
        await logger.LogExceptionAsync(ex,
                                        Constants.LOGGING_DATAKEY_REFRESHACCESSTOKENFAILURE,
                                        currentUserId);
        currentUserId = null;
        AccessToken = null;
    }
}

As stated this results in a 401 Unauthorised response from Azure. I have run Fiddler and the request seems to be correct, the expected authentication token is included in a JSON payload with the request.

UPDATE One thing I can see is that the token issued by the code above is almost 900 characters long, all in the form YnElFkAAcK8bRSQab/FK+PT5n/wA4CPU..., while the token issued if I let Azure Mobile App handle the authentication, i.e. call MobileServiceClient.LoginAsync() without passing a token, is only about 350 characters long and in the form hbGciOi.eyJmdWWxsIiwiRGJn... (notice the period towards the beginning).

This issue is really causing me problems now. I can't release the app without the authentication working and I can't figure out how to fix it. Any help will be appreciated.

like image 718
user994597 Avatar asked Sep 27 '22 14:09

user994597


1 Answers

This was a tough one for me to solve as I was facing this problem too.

The most important part is the OnlineIdServiceTicketRequest the request should look like this:

var mobileServicesTicket = new OnlineIdServiceTicketRequest("https://yourmobileservice.azure-mobile.net/", "JWT");

Note that we are specifying your endpoint and also requesting a JWT token instead of delegation. This will get the 350ish character token you were looking for.

Here is a full code sample of what I'm doing:

public async Task<bool> LoginAsync()
{
  var authenticator = new Windows.Security.Authentication.OnlineId.OnlineIdAuthenticator();
  var mobileServicesTicket = new Windows.Security.Authentication.OnlineId.OnlineIdServiceTicketRequest("https://yourendpoint.azure-mobile.net/", "JWT");

  var ticketRequests = new List<OnlineIdServiceTicketRequest>() { mobileServicesTicket };

  var authResult = await authenticator.AuthenticateUserAsync(ticketRequests, CredentialPromptType.PromptIfNeeded);

  if ((authResult.Tickets.Count == 1) && (authResult.Tickets[0].ErrorCode == 0))
  {                            
      var accessToken = authResult.Tickets[0];          
      var res = await _mobileServiceClient.LoginWithMicrosoftAccountAsync(accessToken.Value);

      return true;
  }
  else
  {          
    return false;
  }
}

_mobileServiceClient is injected into the class and is a reference to Microsoft.WindowsAzure.MobileServices.MobileServiceClient object within the WindowsAzure.MobileServices library.

I actually ended up writing a blog article about this problem here http://jshapland.com/single-sign-on-with-azure-mobile-services-in-a-uwp-app/

like image 120
jmshapland Avatar answered Sep 29 '22 14:09

jmshapland