Secure Data Access with IdentityServer4 and Xamarin Forms

This post is the December 9th entry in the 2018 C# Advent Series. Shout out to Matt for adding me on the roster.

As .NET developers, we will likely run into situations where a client or organization requests an a means for accessing sensitive data. The front-end interface may be a web interface, desktop client, or even a mobile application. These front-ends will access a back-end REST service that pulls in numerous objects from a data source.


How do we secure data access? The easiest answer is to make sure each data request is authenticated with tokens received from an identity framework. To demonstrate a simple way this can be achieved, I'm going to walk-through configuring IdentityServer4 to secure an API that will be consumed by an iOS application built with Xamarin Forms.


Prerequisites


Before we get started, you'll need the following:

  1. Windows or a Mac (This focuses primarily on Windows)
  2. Visual Studio 2017 with the Xamarin iOS and .NET Core 2.1 components installed
  3. A Mac with Xamarin.iOS components installed. If you don't have a Mac, fear not - You should be able to run the mobile-specific steps through Android or the UWP projects
  4. Your local IP Address


Creating the IdentityServer4 Host


IdentityServer4 is a popular, open-source OpenID Connect and OAuth framework built on top of ASP.NET Core and .NET Core. In addition to OpenID Connect and OAuth, it also has support for WS-Federation and SAML2p, but it'll either cost you or require quite a bit of extra coding to make happen. For the sake of brevity, we'll keep it simple with an out of box implementation.

To get started with creating an IdentityServer4 host we're going to utilize the dotnet templates created by the team, but first we have to install them.

Open a Terminal or command prompt window and run the following command:

dotnet new -i IdentityServer4.Templates


This will install a set of project templates that can be used to create ASP.NET Core hosts with pre-built samples for IdentityServer4.


Next, create a folder that the project will live in and run the following command :

dotnet new is4inmem


The project will be created with developer signing certs and in-memory objects. For a production environment, you will need to generate production signing certificates to ensure the token generation is secure. 


After the project is created, open appSettings.json in an editor. This test configuration for the IdentityResources, ApiResources, and Clients are stored in here. Let's make a few modifications:

1. Find the ApiResource named api1 and change it to be values-api.

2. Scroll to the end and change the Clients node to the following, while replacing http://ipaddress:5000/ in the redirectUri section with the actual IP Address of your computer that's hosting the AuthServer:

  "Clients": [
{
"ClientId": "xamarin-client",
"ClientName": "Xamarin Client",
"AllowedGrantTypes": [ "authorization_code" ],
"AllowedScopes": [ "openid", "profile", "values-api" ],
"AllowAccessTokensViaBrowser": true,
"AllowOfflineAccess" :true,
"AlwaysIncludeUserClaimsInIdToken": true,
"RequirePkce": true,
"RequireClientSecret": false,
"RedirectUris": [
"http://ipaddress:5000/grants"
]
}


What we've done is modify the ApiResource to be named values-api, changed the Client to be xamarin-client and changed the scope of this client to be allowed to request the values-api.


Now that we're configured, run dotnet run --urls http://*:5000 in the command prompt, then access http://localhost:5000 in a browser. This will present the default IdentityServer4 interface created from the template.


Let's keep this running and open a second command prompt window so we can create the API.


Making a Secured API


As we've done in the previous portion, we'll need to create another folder but solely to hold the API Server. This time, we'll run the command that will create our API project:

dotnet new webapi



Our API is now created, but we currently configured to serve unauthenticated requests to the built-in ValuesController. To resolve this issue, we're going to add the IdentityServer4.AccessTokenValidation NuGet package. This package will give the API Server the ability to accept a JSON Web Token (JWT) passed in through an Authorization header to determine access.



To configure the authentication, replace the ConfigureServices method with the following in Startup.cs

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddAuthentication("Bearer")
                .AddIdentityServerAuthentication(options =>
                {
                    options.ApiName = "values-api";
                    options.Authority = "http://ipaddress:5000";
                 options.RequireHttpsMetadata = false;
                });

services.AddMvc(options =>
{
options.Filters.Add(new AuthorizeFilter());
});
           
        }


The ApiName property represents the values-api we configured as an ApiResource on the AuthServer and the Authority property represents the url of the AuthServer. It's important that the authority's URL matches the location of the Auth Server, otherwise it will fail due to a difference in audience. Localhost and a local IP address count as different audiences. For simple purposes, I've set RequireHttpsMetadata to false. You wouldn't want to set this in a Production environment. If you wish to get even more secure, extra options exist for controlling scope verification.


In the API project, find the ValuesController within the Controller folder and replace the class with the following code:

    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        // GET api/values
        [HttpGet]
        public IEnumerable<object> Get()
        {
            return new [] {
                new {  Name =  "Value Item 1", Description = "Some Random Desc"},
                new {  Name =  "Value Item 2", Description = "Some Random Desc"},
                new {  Name =  "Value Item 3", Description = "Some Random Desc"},
                new {  Name =  "Value Item 4", Description = "Some Random Desc"}
            };
        }
    }



Back to the command prompt, use the dotnet run --urls=http://*:5001 command to run the API Server.



Access Data from the iOS Application


For this portion, we're going to create a simple Xamarin Forms app from Visual Studio 2017 that targets Xamarin.iOS.


Create a New Project and choose Mobile App (Xamarin.Forms), then select the blank template. Uncheck all platforms that aren't iOS. This will create a Xamarin.Forms .NET Standard 2.0 library and a corresponding Xamarin.iOS project.




Now that our project is created, let's add the following NuGet packages to both projects:

Xamarin.Auth
Xamarin.Auth.Extensions
Xamarin.Auth.XamarinForms
PCLCrypto

Newtonsoft.Json




Once the packages are installed, we'll need to configure Xamarin.Auth. The Xamarin.Auth library is a cross-platform OAuth2 authentication component that uses embedded Web Views in order to authenticate a user.


In the Xamarin Forms project, find the App.xaml.cs file. This is the main entry point for the App. Replace the contents of the App class with the following code, replacing http://ipaddress  (prepare for errors that we'll resolve later):


  public partial class App : Application
    {
        public App()
        {
            InitializeComponent();

            MainPage = new MainPage();
        }

        public static Account AuthAccount { get; set; }
        public static HttpClient Client = new HttpClient();
        protected override void OnStart()
        {
            var oAuth = new OAuth2AuthenticatorEx("xamarin-client", "offline_access values-api",
                new Uri("http://ipaddress:5000/connect/authorize"), new Uri("http://ipaddress:5000/grants"))
            {
                AccessTokenUrl = new Uri("http://ipaddress:5000/connect/token"),
                ShouldEncounterOnPageLoading = false
            };
            var account = AccountStore.Create().FindAccountsForService("AuthServer");
            if (account != null && account.Any())
            {
                AuthAccount = account.First();
                Client.DefaultRequestHeaders.Add("Authorization", $"Bearer {AuthAccount.Properties["access_token"]}");
                MainPage = new ValuesPage();
            }
            else
            {
                var presenter = new OAuthLoginPresenter();
                presenter.Completed += Presenter_Completed;
                presenter.Login(oAuth);
            }
        }

        private void Presenter_Completed(object sender, AuthenticatorCompletedEventArgs e)
        {
            if(e.IsAuthenticated)
            {

                AuthAccount = e.Account;
                Client.DefaultRequestHeaders.Add("Authorization", $"Bearer {AuthAccount.Properties["access_token"]}");
            //    await AccountStore.Create().SaveAsync(e.Account, "AuthServer");
                MainPage = new ValuesPage();
            }
        }

        protected override void OnSleep()
        {
            // Handle when your app sleeps
        }

        protected override void OnResume()
        {
            // Handle when your app resumes
        }
    }


Now that Xamarin.Auth is configured to point to the Auth Server, create a Xamarin Forms List View page in the Xamarin Forms project. This page's purpose is to return an authenticated list of data from the API we created.




In ValuesPage.xaml, replace the contents:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamarinClient.ValuesPage">
  <ListView x:Name="MyListView"
            ItemTapped="Handle_ItemTapped"
            CachingStrategy="RecycleElement">
        <ListView.ItemTemplate>
            <DataTemplate>
                <ViewCell>
                    <StackLayout>
                        <Label Text="{Binding Name}"></Label>
                        <Label Text="{Binding Description}" FontSize="Small"></Label>
                    </StackLayout>
                </ViewCell>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>


In ValuesPage.xaml.cs, replacing http://ipaddress

  [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class ValuesPage : ContentPage
    {
        public class Value
        {
            public string Name { get; set; }
            public string Description { get; set; }
        }
    

        protected override async void OnAppearing()
        {
            var dataRequest = await App.Client.GetAsync("http://ipaddress:5001/api/values");
            if(dataRequest.IsSuccessStatusCode)
            {
                var resultString = await dataRequest.Content.ReadAsStringAsync();
                var resultObject = JsonConvert.DeserializeObject<List<Value>>(resultString);
                MyListView.ItemsSource = resultObject;
            }
            base.OnAppearing();
        }

        public ValuesPage()
        {
            InitializeComponent();
           
          
        }

        async void Handle_ItemTapped(object sender, ItemTappedEventArgs e)
        {
            if (e.Item == null)
                return;

            await DisplayAlert("Item Tapped", "An item was tapped.", "OK");

            //Deselect Item
            ((ListView)sender).SelectedItem = null;
        }
    }


Unfortunately, Xamarin.Auth's OAuth2Authenticator class is missing some consistency, especially with properly supporting IdentityServer4 out of the box, which leaves us to work around it.

Create a new file named OAuth2AuthenticatorEx.cs and replace the class contents with the following:


  
public class OAuth2AuthenticatorEx : OAuth2Authenticator
{
private string _codeVerifier = String.Empty;
private string _redirectUrl = String.Empty;
public OAuth2AuthenticatorEx(string clientId, string scope, Uri authorizeUrl, Uri redirectUrl, GetUsernameAsyncFunc getUsernameAsync = null, bool isUsingNativeUI = false) : base(clientId, scope, authorizeUrl, redirectUrl, getUsernameAsync, isUsingNativeUI)
{
}

public OAuth2AuthenticatorEx(string clientId, string clientSecret, string scope, Uri authorizeUrl, Uri redirectUrl, Uri accessTokenUrl, GetUsernameAsyncFunc getUsernameAsync = null, bool isUsingNativeUI = false) : base(clientId, clientSecret, scope, authorizeUrl, redirectUrl, accessTokenUrl, getUsernameAsync, isUsingNativeUI)
{

}

protected override void OnCreatingInitialUrl(IDictionary<string, string> query)
{
_redirectUrl = Uri.UnescapeDataString(query["redirect_uri"]);
_codeVerifier = CreateCodeVerifier();
query["response_type"] = "code";
query["nonce"] = Guid.NewGuid().ToString("N");
query["code_challenge"] = CreateChallenge(_codeVerifier);
query["code_challenge_method"] = "S256";
base.OnCreatingInitialUrl(query);
}
private string CreateCodeVerifier()
{
var codeBytes = WinRTCrypto.CryptographicBuffer.GenerateRandom(64);
return Convert.ToBase64String(codeBytes).Replace("+", "-").Replace("/", "_").Replace("=", "");
}
private string CreateChallenge(string code)
{
var codeVerifier = code;
var sha256 = WinRTCrypto.HashAlgorithmProvider.OpenAlgorithm(HashAlgorithm.Sha256);
var challengeByteArray = sha256.HashData(WinRTCrypto.CryptographicBuffer.CreateFromByteArray(Encoding.UTF8.GetBytes(codeVerifier)));
WinRTCrypto.CryptographicBuffer.CopyToByteArray(challengeByteArray, out byte[] challengeBytes);
return Convert.ToBase64String(challengeBytes).Replace("+","-").Replace("/","_").Replace("=","");
}

protected override async void OnRedirectPageLoaded(Uri url, IDictionary<string, string> query, IDictionary<string, string> fragment)
{
query["code_verifier"] = _codeVerifier;
query["client_id"] = ClientId;
query["grant_type"] = "authorization_code";
query["redirect_uri"] = _redirectUrl;
var token = await RequestAccessTokenAsync(query);
foreach(var tokenSegment in token)
{
fragment.Add(tokenSegment);
}
base.OnRedirectPageLoaded(url, query, fragment);
}
}




This extension of Xamarin Auth's OAuth2Authenticator class provides an implementation of the Proof Key Code Exchange (PKCE). PKCE is a game changer for mobile authentication by using a code_verifier, which happens to be a Base-64 encoded, random generated string that only the native client knows about. The Auth Server receives a code_challenge containing a transformed version of the client's code_verifier during the authorization_code request, along with the transformation method. As of now, two transformation methods exist: SHA-256 and plain-text. If you haven't figured out by now, plain-text = bad and shouldn't be used. After a successful authorization code is returned, you'll need to request an access token. With the PKCE flow enabled, the client must attach the original code_verifier used to create the transformed code_challenge, in order to retrieve an access token. If the client doesn't have the original, then the access token request will fail. Along with an access token, we will also retrieve a refresh token due to the offline_access scope we have defined in the xamarin-client.


We're almost fully set, except for one last thing that needs done for iOS.


In the Xamarin.iOS project, extra code must be added in order for Xamarin.Auth to recognize the login presenter that handles the the authentication flow through an embedded browser.

Add the replace the FinishedLaunching method in AppDelegate.cs with the following code, resolving any namespace and type errors:


        public override bool FinishedLaunching(UIApplication app, NSDictionary options)
        {
            global::Xamarin.Forms.Forms.Init();
            LoadApplication(new App());
            Xamarin.Auth.Presenters.OAuthLoginPresenter.PlatformLogin = (authenticator) =>
            {
                var oAuthLogin = new PlatformOAuthLoginPresenter();  
                oAuthLogin.Login(authenticator);
            };
            return base.FinishedLaunching(app, options);
        }


Alright, folks. We're ready to go! Fire up an iOS simulator or an iOS device and login with:

username: alice
password: alice.



You'll see a consent screen afterwards that explains the grants this client is requesting. The blank one is the API (and I don't know why it has a blank description).



After you allow the consent, you should see the following screen. If you see a list of values populated, the API request was successful.



Quick Closing Thoughts


Organizations and users have an increased need for secure data access, due to ongoing threats by malicious attackers. One misstep in a system deemed to be secure can easily present a chance to be made a bad example, but luckily the proper tools are available to incorporate.


Completed source located at https://github.com/snickler/Samples