Xamarin.Forms + Azure Mobile Apps 認証処理 Getting Started (2/2)

# 概要
## この記事の記載内容
- Xamarin.Forms + Azure Mobile Appsで認証処理を行う
- 公式ドキュメントだと代替案が大量にあり大混乱したので、余計な代替案は出さず、最低限の説明のみ記載し、認証処理を実現できることを目指す
- 対象OSはAndroid、iOS
- 1章は主にAzureの設定
- 2章はAzure Web Apps、Xamarinのプログラム修正方法（ここ）。公式のReferenceだけではなく、実現したい機能用の処理も追加する

## 実現したい処理内容
1. 複数プロバイダ（Office365, Google）でXamarin.Forms + Azure Mobile Appsの認証処理を行う
2. アプリ起動時、過去にサインインを行っていれば、サインイン画面を再度表示せずに認証後の画面に遷移する
3. アクセストークンは定期的に更新する

## その他・要調査項目
- Azure Mobile Appsにはサーバー側認証する。クライアント側認証との違いは難しいので良くわかってない
- GoogleのRefreshトークンの取得が出来ない
- サインアウト時、アプリからキャッシュ消すだけじゃなくて、ブラウザからもサインアウトしたいけど、やり方わからない

## 今回解説用に作成するアプリの名称
- アプリ名：XamarinAuthFirst
- Azure Mobile Apps名：xamarin-auth-1st
- Azure Mobile Apps URL： https://xamarin-auth-1st.azurewebsites.net
- リダイレクト用URLスキーマ： xamarin-auth-1st://easyauth.callback

## 前回
https://qiita.com/hirossyi73/items/6aab930feea0bbe7da24

# Azure Mobile Appsプログラム修正
前回ダウンロードした、Azure Mobile AppsのC#ソリューション本体を修正します。
はじめてソリューションを開いた場合、NuGetの復元が必要です。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/204834/ef3153f4-6052-c67f-da4b-516c5325aa1c.png)

## 認証Attribute追加
認証を行う必要がある、コントローラーもしくはメソッドに[Authorize]属性を追加します。
今回の場合、すべてのコントローラーに追加して問題ないです。
クイックスタートのアプリの場合、TodoItemController、ValuesControllerの両方に、[Authorize]属性を追加します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/204834/d2a15a69-9cea-b10e-d0d1-66f21a91b078.png)

## 発行
プロジェクトの発行を実施します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/204834/61795ce3-d77f-5480-52cb-4f2a2676b755.png)

AzureのMobile Serviceに、プロジェクトの発行を行ってください。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/204834/fbaddfc5-b52a-092b-7043-1fd966949e6c.png)


# Xamarin.Formsプログラム修正
前回ダウンロードした、Xamarin.Formのクイックスタートプロジェクトを開きます。
Mobile Apps側と同様に、ソリューションのNuget復元を行って下さい。

## PCL修正
共通部分、プロジェクト「xamarin_auth_1st」の修正を行っていきます。
まず、Account処理に関するHelper、AccountHelper.csを作成します。

``` c#:AccountHelper.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Microsoft.WindowsAzure.MobileServices;
using xamarin_auth_1st;
using System.Net.Http;
using System.Threading.Tasks;

// NuGetで追加
using Xamarin.Auth;
namespace Xamarin.Auth
{
    public class AccountHelper
    {
        private const string AppName = "your_app_name";

        /// <summary>
        /// 保持しているMobileServiceUser情報を取得
        /// </summary>
        /// <returns></returns>
        public static MobileServiceUser Get()
        {
            // AccountStoreから取得
            var account = AccountStore.Create().FindAccountsForService(AppName).FirstOrDefault();
            // 存在すれば返却
            if (account != null)
            {
                return JsonConvert.DeserializeObject<MobileServiceUser>(account.Properties["MobileServiceUser"]);
            }
            // なければnull返却
            return null;
        }

        /// <summary>
        /// MobileServiceUser情報をAccountStoreにセット
        /// </summary>
        /// <param name="user"></param>
        public static void Set(MobileServiceUser user)
        {
            // Account作成
            Account account = new Account
            {
                Username = user.UserId
            };
            // MobileServiceUserをjson形式に変換して追加
            account.Properties.Add("MobileServiceUser", JsonConvert.SerializeObject(user));

            // すでにあるAccountを削除
            Clear();

            // 保存
            AccountStore.Create().Save(account, AppName);
        }

        /// <summary>
        /// AccountStoreのクリア
        /// </summary>
        public static void Clear()
        {
            var accounts = AccountStore.Create().FindAccountsForService(AppName);
            if (accounts != null)
            {
                foreach (var account in accounts)
                {
                    AccountStore.Create().Delete(account, AppName);
                }
            }
        }

        /// <summary>
        /// トークンのリフレッシュ。MobileServiceのアクセストークンは2時間で切れるので、接続前に最新化する
        /// </summary>
        public static async void Refresh()
        {
            using (var client = new HttpClient())
            {
                try
                {
                    // CurrentUser取得
                    var user = TodoItemManager.DefaultManager.CurrentClient.CurrentUser;
                    if (user == null)
                    {
                        return;
                    }
                    // ヘッダー情報などセット
                    client.DefaultRequestHeaders.Add("X-ZUMO-AUTH", user.MobileServiceAuthenticationToken);
                    // トークン最新化
                    var response = await client.GetAsync(Constants.ApplicationURL + "/.auth/refresh");
                    if (response.IsSuccessStatusCode)
                    {
                        // 取得したjsonを変換し、CurrentUserにセット
                        JToken jObject = JToken.Parse(await response.Content.ReadAsStringAsync());
                        user.MobileServiceAuthenticationToken = Convert.ToString(jObject["authenticationToken"]);
                        Set(user);
                    }
                }
                catch (Exception)
                {
                }
            }
        }

        /// <summary>
        /// アプリ起動時に実施。AccountStoreにユーザー情報あれば、CurrentUserにセット
        /// </summary>
        /// <returns></returns>
        public static void OnInitialized()
        {
            // MobileServiceUserを取得
            var user = Get();
            // 未ログインであれば終了
            if (user == null)
            {
                return;
            }

            // MobileServiceUserをセット
            TodoItemManager.DefaultManager.CurrentClient.CurrentUser = user;
        }

        /// <summary>
        /// サインイン済かどうか
        /// </summary>
        /// <returns></returns>
        public static bool IsAuthenticated()
        {
            return TodoItemManager.DefaultManager.CurrentClient.CurrentUser != null;
        }
    }
}
```
標準のMicrosoft.WindowsAzure.MobileServicesではおそらく用意されていない、Account保持や、トークン更新に関する処理を、このクラスで定義しています。

次に、IAuthenticateインタフェースを定義します。

``` c#:App.cs
using System.Threading.Tasks;
using Microsoft.WindowsAzure.MobileServices;

namespace xamarin_auth_1st
{
    /// <summary>
    /// Authentication Interface
    /// </summary>
    public interface IAuthenticate
    {
        Task<bool> AuthenticateAsync(MobileServiceAuthenticationProvider provider);
    }  
    public class App : Application
    {
        // 略

        // 追加
        public static IAuthenticate Authenticator { get; private set; }
        public static void Init(IAuthenticate authenticator)
        {
            Authenticator = authenticator;
            //AccountStoreからMobileServiceUserにセット
            Xamarin.Auth.AccountHelper.OnInitialized();
        }
    }
}
```
公式ドキュメントだと、AuthenticateAsyncは引数を持っていないのですが、これだと1つのプロバイダにしか対応できないので、列挙体MobileServiceAuthenticationProviderを引数としています。

次に、TodoItemManagerを修正します。

``` c#:TodoItemManager.cs
namespace xamarin_auth_1st
{
    public partial class TodoItemManager
    {
        public async Task<ObservableCollection<TodoItem>> GetTodoItemsAsync(bool syncItems = false)
        {
            try
            {
                // トークン最新化
                Xamarin.Auth.AccountHelper.Refresh();

                // 略
            }
         }

        public async Task SaveTaskAsync(TodoItem item)
        {
            // トークン最新化
            Xamarin.Auth.AccountHelper.Refresh();

            // 略
        }
    }
}
```
Mobile Appsと通信を行う前に、トークンのRefreshを行い、期限切れを防ぐようにしています。
MobileServiceClient.RefreshUserAsync()という関数もあるのですが、どうにもうまくいかないことが多かったので、自分はHttpClient使っています。

次に、サインインボタンを追加します。

``` c#:TodoList.xaml
<Button x:Name="loginButton_aad" Text="Sign-in AAD" MinimumHeightRequest="30"
     Clicked="loginButton_aad_Clicked"/>
<Button x:Name="loginButton_google" Text="Sign-in google" MinimumHeightRequest="30"
     Clicked="loginButton_google_Clicked"/>
<Button x:Name="logoutButton" Text="Sign-out" MinimumHeightRequest="30"
     Clicked="logoutButton_Clicked"/>
```
このXMLを、適当な場所に追加して下さい。


``` c#:TodoList.xaml.cs
using Xamarin.Auth;
using Microsoft.WindowsAzure.MobileServices;

namespace xamarin_auth_1st
{
    public partial class TodoList : ContentPage
    {
        // 追加
        bool authenticated
        {
            get
            {
                return AccountHelper.IsAuthenticated();
            }
        }

        public TodoList()
        {
            InitializeComponent();

            // 略

            // 追加。AccountStoreからMobileServiceUserをセット
            AccountHelper.OnInitialized();
        }

        // 略

        private async Task RefreshItems(bool showActivityIndicator, bool syncItems)
        {
            // サインインボタンの表示非表示の切り替え
            this.FindByName<Button>("loginButton_aad").IsVisible = !authenticated;
            this.FindByName<Button>("loginButton_google").IsVisible = !authenticated;
            this.FindByName<Button>("logoutButton").IsVisible = authenticated;
            // サインイン済であれば、データ取得
            if (authenticated)
            {
                using (var scope = new ActivityIndicatorScope(syncIndicator, showActivityIndicator))
                {
                    todoList.ItemsSource = await manager.GetTodoItemsAsync(syncItems);
                }
            }
            // 未サインインであれば、クリア
            else
            {
                (todoList.ItemsSource as System.Collections.ObjectModel.ObservableCollection<TodoItem>)?.Clear();
            }
        }

        // 追加
        async Task login(MobileServiceAuthenticationProvider provider)
        {
            if (App.Authenticator != null)
                authenticated = await App.Authenticator.AuthenticateAsync(provider);

            // Set syncItems to true to synchronize the data on startup when offline is enabled.
            if (authenticated == true)
            {
                // サインイン者の情報をセット
                AccountHelper.Set(TodoItemManager.DefaultManager.CurrentClient.CurrentUser);
                await RefreshItems(true, syncItems: false);
            }
        }
        async void loginButton_aad_Clicked(object sender, EventArgs e)
        {
            await login(MobileServiceAuthenticationProvider.WindowsAzureActiveDirectory);
        }
        async void loginButton_google_Clicked(object sender, EventArgs e)
        {
            await login(MobileServiceAuthenticationProvider.Google);
        }

        async void logoutButton_Clicked(object sender, EventArgs e)
        {
            AccountHelper.Clear();
            await TodoItemManager.DefaultManager.CurrentClient.LogoutAsync();
            await RefreshItems(true, false);
        }
        // 略
    }
}
        
```
色々修正しましたが、やっていることは主に以下の内容です。
・読み込み時、MobileServiceUserのサインイン状態取得
・RefreshItems時、サインイン状態に従って、サインインのボタンの表示非表示を切り替える
　また、サインイン済であればリスト取得、そうでなければリスト削除
・サインインボタン押下時、AuthenticateAsync(各プラットフォーム側で実装)を実行
・サインイン完了したら、AccountStoreにサインイン情報を保持してRefreshItems
・サインアウトした時、AccountStoreからサインイン情報を削除してRefreshItems

PCLの修正は以上です。

## Android修正
続いてAndroid側の修正を行います。

``` c#:MainActivity.cs
using System.Threading.Tasks;
namespace xamarin_auth_1st.Droid
{
	public class MainActivity : FormsApplicationActivity, IAuthenticate
	{
		protected override void OnCreate (Bundle bundle)
		{
			// 略

			// Load the main application
			LoadApplication (new App ());

			// 追加
			// Initialize the authenticator before loading the app.
			App.Init((IAuthenticate)this);
		}
        // 追加
        // Define a authenticated user.
        private MobileServiceUser user;
        public async Task<bool> AuthenticateAsync(MobileServiceAuthenticationProvider provider)
        {
            var success = false;
            var message = string.Empty;
            try
            {
                // Sign in with Facebook login using a server-managed flow.
                user = await TodoItemManager.DefaultManager.CurrentClient.LoginAsync(this, provider, "xamarin-auth-1st");
                if (user != null)
                {
                    message = string.Format("you are now signed-in as {0}.",
                        user.UserId);
                    success = true;
                }
            }
            catch (Exception ex)
            {
                message = ex.Message;
            }

            // Display the success or failure message.
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.SetMessage(message);
            builder.SetTitle("Sign-in result");
            builder.Create().Show();

            return success;
        }
	}
}
```
MainActivity.csに、サインイン時の処理を記載していきます。

``` AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.xamarin.sample.xamarin_auth_1st">
	<uses-sdk android:minSdkVersion="15" android:targetSdkVersion="25" />
	<application android:label="xamarin_auth_1st" android:icon="@drawable/icon">
        <!-- 追加 -->
        <activity android:name="com.microsoft.windowsazure.mobileservices.authentication.RedirectUrlActivity" android:launchMode="singleTop" android:noHistory="true">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="xamarin-auth-1st" android:host="easyauth.callback" />
            </intent-filter>
        </activity>
        <!-- /追加 -->
	</application>
</manifest>
```
マニフェストファイルに、URLスキームの情報を記載します。
これで、Android側の実装は完了です。


## iOS修正
すみません、調査中です。


# まとめ
このような手順で、Xamarin.FormsとAzure Mobile Appsのサインイン処理実装が完了します。
実際にアプリを組む場合、おそらく（というか間違いなく）プロバイダから、ユーザー名やEメールアドレス、プロバイダのアクセストークン取得など行う必要あると思いますが、
その辺の解説はまた別途、機会があれば行いたいと思っています。

# 参考文献
https://docs.microsoft.com/ja-jp/azure/app-service-mobile/app-service-mobile-xamarin-forms-get-started-users
https://docs.microsoft.com/ja-jp/azure/app-service/app-service-mobile-how-to-configure-active-directory-authentication
https://docs.microsoft.com/ja-jp/azure/app-service/app-service-mobile-how-to-configure-google-authentication

# （宣伝1）Vietnam Japan Cloud Developer 2017 開催！
![c22.png](https://qiita-image-store.s3.amazonaws.com/0/204834/56188a7c-5c40-e30e-e255-42374ff78563.png)
https://connpass.com/event/67792/
Xamarin.FormsやAzure Mobile Appsをはじめとした、クラウドサービスのセミナーをハノイで開催します！
日程：2017/11/17(Fri) 16:00-20:00
自分も少し話します。英語か日本語かは要検討。
内容はYouTubeにも公開します。ハノイには来れない、という方もぜひYouTubeにてお越し下さい！

# （宣伝2）Xamarin.Forms + Azure Mobile AppsのOSSプロジェクト開発中
現在、Xamarin.Forms + Azure Mobile AppsのOSSアプリを走らせています。
日々の日報などをSPのみで完結出来るアプリです。
GitHubで公開しておりますので、よければぜひご覧ください！
https://github.com/exceedone/APO-Chan