Xamarin.Auth を使って Xamarin.Forms 製アプリを OAuth2 に対応させる

はじめに

Web サービスが提供する API を使ったクライアントアプリを開発する場合、 API の認証には OAuth を使うことが多い。

アプリで OAuth 対応を行う場合、肝心の認証部分は WebView を使った埋め込みブラウザを使うよりは、 SFSafariViewController や Chrome Custom Tabs を使うほうが望ましい。

それは Xamarin 製アプリでも同じ。

Xamarin.Auth

Xamarin.Auth という、Xamarin 製アプリに OAuth を使う認証機能を組み込むためのライブラリがある。 開発は活発ではないものの、メンテは続いてそう。 Deprecate にはなってないし。

github.com

Xamarin.Auth は認証時にネイティブの UI を使えるようになっていて、iOS なら SFSafariViewController、Android なら Chrome Custom Tabs を使ってくれる。

この Xamarin.Auth を使って Xamarin.Forms 製アプリを OAuth に対応させてみた。 だいぶ前に Xamarin.iOS プロジェクトに組み込もうとして上手くいかず断念した過去があり、 今回はそのリベンジ。

事前準備

GoogleFacebookGitHub などの OAuth サービスプロバイダにアプリ情報を登録し、 クライアント ID とクライアントシークレットを取得しておく。

アプリ情報登録の際に指定するリダイレクト URL は、カスタム URL スキーマのものを指定しておく。

Xamarin.Auth のインストール

NuGet で Xamarin.Auth をインストールする。

www.nuget.org

Xamarin.Forms プロジェクト側の実装

状態を共有するためのクラスを用意する

Xamarin.iOS 側と Xamarin.Android 側で、リダイレクト URL が呼ばれた後に処理を続行できるように、状態を共有するためのクラスを用意する。

using Xamarin.Auth;

public static class AuthenticationState
{
    public static OAuth2Authenticator Authenticator { get; set; }
}
OAuth2 での認証を開始するメソッドを実装する

Xamarin.Auth の OAuth2Authenticator を使用。

using Xamarin.Auth;
using Xamarin.Auth.Presenters;

[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MainPage
{
    // 省略

    void ShowLoginPage()
    {
        var authenticator = new OAuth2Authenticator(
            clientId: "{OAuth サービスプロバイダから取得したクライアント ID}",
            clientSecret: "{OAuth サービスプロバイダから取得したクライアントシークレット}",
            scope: "{必要なスコープ}",
            authorizeUrl: new Uri("{OAuth サービスプロバイダの認証 URL}"),
            redirectUrl: new Uri("{OAuth サービスプロバイダに登録したリダイレクト URL}"),
            accessTokenUrl: new Uri("{OAuth サービスプロバイダに登録したアクセストークン取得 URL}"),
            isUsingNativeUI: true); // iOS では SFSafariViewController、Android では Chrome Custom Tabs を使う

        // AllowCancel が true だと Android の WebAuthenticatorNativeBrowserActivity.OnResume
        // で Authenticator の OnCancelled を呼び出してしまい、Completed イベントの
        // e.IsAuthenticated が false になってしまっていた。
        authenticator.AllowCancel = false;

        authenticator.ClearCookiesBeforeLogin = true;
        authenticator.Completed += Authenticator_Completed;
        authenticator.Error += Authenticator_Error;
        AuthenticationState.Authenticator = authenticator;

        var presenter = new OAuthLoginPresenter();
        presenter.Login(authenticator);
    }

    async void Authenticator_Completed(object sender, AuthenticatorCompletedEventArgs e)
    {
        if (sender is OAuth2Authenticator authenticator)
        {
            authenticator.Completed -= Authenticator_Completed;
            authenticator.Error -= Authenticator_Error;
        }

        // アクセストークは Account.Properties から取り出せる。
        if (e.IsAuthenticated &&
            e.Account.Properties.TryGetValue("access_token", out var accessToken))
        {
            // TODO: アクセストークンを保存する
        }
    }

    void Authenticator_Error(object sender, AuthenticatorErrorEventArgs e)
    {
        if (sender is OAuth2Authenticator authenticator)
        {
            authenticator.Completed -= Authenticator_Completed;
            authenticator.Error -= Authenticator_Error;
        }
    }
}

このメソッドを、認証を行いたいタイミングで呼び出すようにする。

Xamarin.iOS プロジェクト側の実装

Custom URL Schema を設定する

Info.plist にカスタム URL スキーマを追加する。 OAuth サービスプロバイダに登録したリダイレクト URL の URL スキーマを追加すること。

AppDelegate.OpenUrl メソッドを実装する

SFSafariViewController での認証後にリダイレクトされると、 AppDelete の OpenUrl が呼び出される。 コールバックに指定した URL が呼び出されたのかどうかをチェックし、 もしそうだった場合は OAuth2Authenticator の処理の続きを実行する。

using System;
using Foundation;
using UIKit;

[Register("AppDelegate")]
public class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
    public override bool FinishedLaunching(UIApplication uiApplication, NSDictionary launchOptions)
    {
        global::Xamarin.Forms.Forms.SetFlags("CollectionView_Experimental");

        global::Xamarin.Forms.Forms.Init();

        global::Xamarin.Auth.Presenters.XamarinIOS.AuthenticationConfiguration.Init();

        LoadApplication(new App());

        return base.FinishedLaunching(uiApplication, launchOptions);
    }

    public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
    {
        if (url.AbsoluteString.StartsWith("{OAuth サービスプロバイダに登録したコールバック URL}", StringComparison.OrdinalIgnoreCase))
        {
            AuthenticationState.Authenticator?.OnPageLoading(new Uri(url.AbsoluteString));
        }
        return true;
    }
}

Xamarin.Android プロジェクト側の実装

Chrome Custom Tabs での認証後にリダイレクトされると呼び出される Activity を実装する。 この Activity は OAuth2Authenticator の処理を呼び出したら即終了する。

using System;
using System.Text;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;

[Activity(Label = "CustomUrlSchemeInterceptorActivity", NoHistory = true, LaunchMode = LaunchMode.SingleTop)]
[IntentFilter(
    actions: new[] { Intent.ActionView },
    Categories = new[]
    {
        Intent.CategoryDefault,
        Intent.CategoryBrowsable
    },
    DataSchemes = new[]
    {
        "{OAuth サービスプロバイダに登録したコールバック URL の URL スキーマ}",
    },
    DataPaths = new[]
    {
        "{OAuth サービスプロバイダに登録したコールバック URL のパス部分}",
    }
)]
public class CustomUrlSchemeInterceptorActivity : Activity
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        var uri = new Uri(Intent.Data.ToString());

        AuthenticationState.Authenticator?.OnPageLoading(uri);

        Finish();
    }
}

おわりに

最初、Android だと認証後のリダイレクトが上手くいかず、リベンジ失敗の文字が頭をよぎった。 そこで諦めず、Xamarin.Auth のソースコードを追っていって、なんとかとか解決。 リベンジ成功。

ただ、Xamarin.Auth 自体が結構重厚なライブラリなので、 SDK の更新で容易に壊れてしまいそうな恐怖感がある。 対応するサービスプロバイダが 1 つだけなら自前で実装したほうがいいのかも。 そう思って、Xamarin.Essentials を使って実装を試みたけど、 リダイレクトしてから Xamarin.Forms 側のページに戻ったら、 ナビゲーションスタックが変になってしまった。断念。 このアプローチは Xamarin.Forms の実装に詳しくならないと厳しそうだ。