概要
この記事の記載内容
- Xamarin.Forms + Azure Mobile Appsで認証処理を行う
- 公式ドキュメントだと代替案が大量にあり大混乱したので、余計な代替案は出さず、最低限の説明のみ記載し、認証処理を実現できることを目指す
- 対象OSはAndroid、iOS
- 1章は主にAzureの設定
- 2章はAzure Web Apps、Xamarinのプログラム修正方法(ここ)。公式のReferenceだけではなく、実現したい機能用の処理も追加する
実現したい処理内容
- 複数プロバイダ(Office365, Google)でXamarin.Forms + Azure Mobile Appsの認証処理を行う
- アプリ起動時、過去にサインインを行っていれば、サインイン画面を再度表示せずに認証後の画面に遷移する
- アクセストークンは定期的に更新する
その他・要調査項目
- 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
前回
Azure Mobile Appsプログラム修正
前回ダウンロードした、Azure Mobile AppsのC#ソリューション本体を修正します。
はじめてソリューションを開いた場合、NuGetの復元が必要です。
認証Attribute追加
認証を行う必要がある、コントローラーもしくはメソッドに[Authorize]属性を追加します。
今回の場合、すべてのコントローラーに追加して問題ないです。
クイックスタートのアプリの場合、TodoItemController、ValuesControllerの両方に、[Authorize]属性を追加します。
発行
AzureのMobile Serviceに、プロジェクトの発行を行ってください。
Xamarin.Formsプログラム修正
前回ダウンロードした、Xamarin.Formのクイックスタートプロジェクトを開きます。
Mobile Apps側と同様に、ソリューションのNuget復元を行って下さい。
PCL修正
共通部分、プロジェクト「xamarin_auth_1st」の修正を行っていきます。
まず、Account処理に関するHelper、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インタフェースを定義します。
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を修正します。
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使っています。
次に、サインインボタンを追加します。
<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を、適当な場所に追加して下さい。
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側の修正を行います。
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に、サインイン時の処理を記載していきます。
<?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 開催!
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