kamulog

xamarin.formsのネタなど

サインインだけで使う場合の Google OAuth 同意画面 の設定について

Google OAuth 同意画面の意図せぬ審査にかなり手こずったので、ここにまとめておきます。

事の発端

復習帳というアプリで Azure AD B2C 経由で Googleのサインインで OAuth 2.0 クライアントを使っています。

結構前から Azure AD B2C で使うリダイレクトURIドメインが microsoftonline.com から b2clogin.com に変わっていて 私はそれより前に設定していた microsoftonline.com で使っていたんですが、移行期限もせまり b2clogin.com に変更しようとしました。

現行バージョンと更新バージョンそれぞれで動作するためには一時的に両方のリダイレクトURIを設定する必要があります。 その作業をtwitterfacebookなどに順番に追加設定していきました。

そして何の迷いもなく、Googleの方も追加設定したところGoogleから 「審査を開始するよ、ただしアプリアイコンを使わない場合はスキップできるよ」 的なメールがきました。

アイコンは不要なので「スキップお願いします」と返信。

すると 「アイコンは一度設定すると削除できません。あなたはアイコンを設定しているので検証の対象になりました。スキップする場合、未検証のスコープにアクセスするなら、未検証アプリ画面が表示されます。未検証アプリ画面を消すには検証が必要です。」 みたいな返信が来て未検証アプリ画面はちょっと困るなということで検証を続けてもらうことにしました。

そこからはプライバシーポリシーのダメ出しやリダイレクトのドメインは所有権のあるもの以外は消してくれだとか結構な無茶を言われて、毎回「いやサインインにしか使わなくてしかも外部サービスなのでドメインは消せない」みたいな返信をするみたいなやりとりが数日間続いてしまいました。 最終的にはRejectになって 「アイコンも必要なくサインインのスコープのみを使用しているようなので検証は必要ありません。」という結末になりました。

Rejectされても今まで通り使用できています。

どうすれば良かったか

大前提としてサインインのみのスコープを使っているだけなら検証は不要です。しかしアイコンを設定してしまうとこの条件だとしてももれなく検証が始まってしまいます。 なのでサインインのみで利用する場合は絶対にアイコンを設定してはいけません。一度設定すると絶対に削除できないです。

また検証が始まってしまった場合でもスキップしてもらえば大丈夫だと思います。 未確認アプリの画面が表示される条件は未検証のスコープにアクセスする場合で、サインインのみのスコープは検証が必要ないため、メールでいろいろ警告されますが気にせずスキップ続行で良いと思います。

私の場合は未確認アプリの画面がでてしまうことにビビってしまって続行してしまいましたが、続行してしまうと無限ループになりかけるので本当に注意が必要です。

まとめ

Google OAuth をサインインのみで利用している場合は

  • 絶対にアイコンを設定してはいけない
  • もし設定してしまったら審査をスキップする

で対応できると思います。

補足

アイコンの削除に関して、以下のかなりグレーな方法は現在は当然ながら無効でした。

stackoverflow.com

Xamarin.Forms で ダークモードにほぼ完全に対応し、「端末の設定を使う」にも対応する

Twitterアプリにある、ダークモード対応の

  • ライト
  • ダーク
  • 端末の設定を使う

の3択パターンを Xamarin.Forms で実装してみました。

サンプル

github.com

youtu.be

Forms 側の対応

Xamarin.Formsでのダークモード対応は現時点でいくつかの選択肢があります。

  1. アプリケーションのシステムテーマの変更に応答する Xamarin.FormsRespond to system theme changes in Xamarin.Forms applications - Xamarin | Microsoft Docs
  2. アプリケーションのテーマを Xamarin.Forms 適用するTheme a Xamarin.Forms Application - Xamarin | Microsoft Docs

1は、まだ試験段階ですが、「端末の設定を使う」だけで良いなら一番簡単だと思います。 ただし、端末はダークだけど、アプリはライトとかの要件だと1では対応しきれないので、今回は2のテーマ切り替えの方法を使いました。

やり方はリンク先のドキュメントに詳しく載っているので割愛しますが、ライト・ダークに対応するResourceDictionaryを作って、それを動的に切り替えるというものです。

追記

Xamarin.Forms 4.7 で現在のシステムのテーマを切り替えることができるようになったみたいなので多分もっと簡単に実装できるかも知れません。

Respond to system theme changes in Xamarin.Forms applications - Xamarin | Microsoft Docs

ただ、この記事の方法だとライト2種、ダーク2種とかのテーマを切り替えることも可能です…

Forms側だけの限界

画像の下部分はSwitchとDatePickerですが

iOSではSwitchの背景が黒くて背景に溶け込みすぎて見にくいのと、DatePickerは背景が白いままです。また画像は省略しますがPickerのドラムみたいな部品も明るいままです。 AndroidではSwitchも同じように見にくく、DatePickerは真っ黒で見えず、DatePickerの下線の色もPlatform側のStyleがそのままなので変化せず、ダイアログは完全にライトモードのままです。

f:id:kamusoft:20200618004007p:plainf:id:kamusoft:20200617213953p:plain

このように、Forms側のテーマだけを変えても対応できない部分があります。 このような部分はシステム側が持っている現在のモードを変更してあげる必要があります。

プラットフォーム実装

NETStandard

現在のシステムテーマを返すものと、システムテーマを上書きするものを定義します。

    public interface ISystemTheme
    {
        void SetTheme(Theme theme);
        Theme GetTheme();
    }

iOS

Interfaceを実装します。

[assembly: Dependency(typeof(SystemThemeImplementation))]
namespace DarkSample.iOS.Platforms
{
    public class SystemThemeImplementation:ISystemTheme
    {
        public Theme GetTheme()
        {
            return UITraitCollection.CurrentTraitCollection.UserInterfaceStyle switch
            {
                UIUserInterfaceStyle.Dark => Theme.Dark,
                _ => Theme.Light
            };
        }

        public void SetTheme(Theme theme)
        {
            UIApplication.SharedApplication.KeyWindow.OverrideUserInterfaceStyle = theme switch
            {
                Theme.Dark => UIUserInterfaceStyle.Dark,
                Theme.Light => UIUserInterfaceStyle.Light,
                _ => UITraitCollection.CurrentTraitCollection.UserInterfaceStyle
            };            
        }
    }
}

Android

iOSと同様に実装します。 ただしこちらはSet時に設定値を保存してます。 これは以下の記事にあるように、システムのテーマをForms初期化前に設定する必要があるからです。

【Android】ダークテーマを反映するアプリ起動時の適切なタイミングについて【Kotlin】 - Qiita

[assembly: Dependency(typeof(SystemThemeImplementation))]
namespace DarkSample.Droid.Platforms
{
    public class SystemThemeImplementation:ISystemTheme
    {
        public static Context Context;

        public Theme GetTheme()
        {
            var currentMode = Context.Resources.Configuration.UiMode & UiMode.NightMask;
            return currentMode switch
            {
                UiMode.NightYes => Theme.Dark,
                _ => Theme.Light
            };
        }

        public void SetTheme(Theme theme)
        {
            AppCompatDelegate.DefaultNightMode = theme switch
            {
                Theme.Dark => AppCompatDelegate.ModeNightYes,
                Theme.Light => AppCompatDelegate.ModeNightNo,
                _ => AppCompatDelegate.ModeNightFollowSystem,
            };            

            Context.GetSharedPreferences("AppSettings", FileCreationMode.Private)
                .Edit()
                .PutInt("NightKey", AppCompatDelegate.DefaultNightMode)
                .Apply();
        }
    }
}

またAndroidはダイアログや、AndroidのStyleが出てしまう部分を対応するためにStyleもダークテーマに対応させます。 以下の記事を参考にして設定しました。

Androidでダークテーマを試す - URL Memo - Medium

Xamarin.FormsのテンプレートのStyleに修正を加えたものです。 Theme.AppCompat.DayNight を使うのがポイントで、これにするとシステムのテーマに応じて色を動的に変更できます。 色の定義はダーク用の values-night/colors.xml で定義します。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="MainTheme" parent="MainTheme.Base">
    </style>
    <!-- Base theme applied no matter what API -->
    <style name="MainTheme.Base" parent="Theme.AppCompat.DayNight">
        <!--If you are using revision 22.1 please use just windowNoTitle. Without android:-->
        <item name="windowNoTitle">true</item>
        <!--We will be using the toolbar so no need to show ActionBar-->
        <item name="windowActionBar">false</item>
        <!-- Set theme colors from https://aka.ms/material-colors -->
        <!-- colorPrimary is used for the default action bar background -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <!-- colorPrimaryDark is used for the status bar -->
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <!-- colorAccent is used as the default value for colorControlActivated
         which is used to tint widgets -->
        <item name="colorAccent">@color/colorAccent</item>
        <!-- You can also set colorControlNormal, colorControlActivated
         colorControlHighlight and colorSwitchThumbNormal. -->
        <item name="windowActionModeOverlay">true</item>
        <!-- 下の3つを設定すれば大体のダイアログに対応できるはず -->
        <item name="android:datePickerDialogTheme">@style/AppCompatDialogStyle</item>
        <item name="android:alertDialogTheme">@style/AppCompatDialogStyle</item>
        <item name="android:timePickerDialogTheme">@style/AppCompatDialogStyle</item>
    </style>
    <style name="AppCompatDialogStyle" parent="Theme.AppCompat.DayNight.Dialog">
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:textColorPrimary">@color/normal_text</item>
        <item name="android:windowBackground">@color/dialog_back</item>
    </style>
</resources>

起動時に設定値を反映させます。あとステータスバーのアイコンの色が見えなくならないようにモードに対応して変更させています。 それについては中の人ブログ参照

Setting Android Status Bar Background & Icon Colors - James Montemagno

public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            var theme = GetSharedPreferences("AppSettings", FileCreationMode.Private)
                .GetInt("NightKey", AppCompatDelegate.ModeNightNo);
            AppCompatDelegate.DefaultNightMode = theme;

            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;            

            base.OnCreate(savedInstanceState);

           ...
            if (Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.M)
            {
                // モードによってステータスバーのアイコンのモードを変える。
                var flag = (Android.Views.StatusBarVisibility)Android.Views.SystemUiFlags.LightStatusBar;
                var uimode = Resources.Configuration.UiMode & UiMode.NightMask;
                Window.DecorView.SystemUiVisibility = uimode == UiMode.NightNo ? flag : 0;
            }

変更処理実装

適当なstaticメソッドなどでテーマの変更を処理する。 似たようなメソッドが二つありますが、ApplyThemeの方は起動時用で、 起動時はAndroidiOSで微妙に処理が異なるので専用のメソッドにしました。 ChangeThemeの方は起動中に切り替える場合に使い、設定を保存しておくようにします。

「端末の設定を使う」に対応するために、システムテーマを取得して、それをFormsテーマに反映させる処理を入れています。 アプリを起動したまま、ホームに戻り、システムの変更を変更した場合に対応できるようにOnResumeのタイミングで前回テーマと変化があれば切り替えるようにしています。

(AndroidAndroid 10以降のみ機能します。)

public static class ThemeResource
{
    static Theme LastSystemTheme = Theme.Light;

    public static void ApplyTheme()
    {
        // 保存設定読み出し
        var theme = (Theme)Xamarin.Essentials.Preferences.Get("mode", (int)Theme.Light);
        var sysTheme = DependencyService.Get<ISystemTheme>();
        if (Device.RuntimePlatform == Device.iOS)
        {
            // iOSのみシステムテーマ変更
            sysTheme.SetTheme(theme);
            // AndroidはPlatform側の起動時に設定
        }

        if (theme == Theme.Auto)
        {
            theme = sysTheme.GetTheme();
            // Autoであれば端末のモード切り替えを待ち受ける
            App.OnForeground += App_OnForeground;
            LastSystemTheme = theme;
        }

        SetDictionary(theme);
    }

    public static void ChangeTheme(Theme theme)
    {
        Xamarin.Essentials.Preferences.Set("mode", (int)theme);

        var sysTheme = DependencyService.Get<ISystemTheme>();

        // PlatformのThemeを変更
        sysTheme.SetTheme(theme);

        App.OnForeground -= App_OnForeground;
        if (theme == Theme.Auto)
        {
            theme = sysTheme.GetTheme();
            // Autoであれば端末のモード切り替えを待ち受ける
            App.OnForeground += App_OnForeground;
            LastSystemTheme = theme;
        }

        SetDictionary(theme);
    }

    static void SetDictionary(Theme theme)
    {
        var mergedDict = Application.Current.Resources.MergedDictionaries;
        mergedDict.Clear();

        switch (theme)
        {
            case Theme.Light:
                mergedDict.Add(new LightTheme());
                break;
            case Theme.Dark:
                mergedDict.Add(new DarkTheme());
                break;
        }
    }

    private static void App_OnForeground(object sender, EventArgs e)
    {
        var sysTheme = DependencyService.Get<ISystemTheme>();
        var curTheme = sysTheme.GetTheme();
        // 前回とテーマが変わらなければ何もしない
        if (curTheme == LastSystemTheme)
        {
            return;
        }

        // PlatformのThemeを変更
        sysTheme.SetTheme(curTheme);

        SetDictionary(curTheme);

        LastSystemTheme = curTheme;
    }

}

起動時にテーマを適用するように。

public partial class App : Application
{
    public static event EventHandler OnForeground;

    public App()
    {
        InitializeComponent();

        MainPage = new MyNavigationPage(new MainPage());
    }

    protected override void OnStart()
    {
        // 起動時テーマ適用
        ThemeResource.ApplyTheme();
    }

    protected override void OnResume()
    {
        OnForeground?.Invoke(this, EventArgs.Empty);
    }
}

結果

完全にダークになりました。分かりにくいですがiOSのSwitchも見やすくなっています。Androidのダイアログにもダークが反映されています。

f:id:kamusoft:20200618003635p:plainf:id:kamusoft:20200618003651p:plain

終わりに

iOSの方は比較的分かりやすかったです。AndroidはStyleを多少いじらないといけなかったり少し面倒でした。 あとAndroidでActivity属性に

ConfigurationChanges = ConfigChanges.UiMode

みたいに UiModeを入れてるとモードを切り替えてもActivityの再生成が起こらず、テーマが反映されないので注意が必要です。 これでかなりハマってしまいました。これにすると OnConfigurationChanged で変更を検知できるようになりますが、Activityの処理は自分でやれよってことみたいです。 そしてよく見たらそうドキュメントにも書いてあったという…

ダークテーマ  |  Android デベロッパー  |  Android Developers

あとはCollectionViewなどのリスト系のUIは即時反映されない場合があるので、その場合は切り替えのタイミングでリロードする必要があるかも知れません。

Sign in with Apple を Azure AD B2C で使う時にどハマりしたこと

Azure Active Directory B2C を復習帳というアプリで利用してます。 これを導入した理由は、認証がめんどくさくて丸投げしたくて、Functionsなど他でAzure使ってるので 合わせてAzureで良いかという安易な理由からでした。

さて、Sign in with Apple の必須化までもう少しです。 てっきり私は、AD B2C も対応するだろうと思ってました。 しかし、このギリギリになってもサポートなし。 一応 カスタム OpenID で追加可能なサンプルはあるものの、他の主要プロバイダーのように公式のテンプレートはありません。

samples/policies/sign-in-with-apple at master · azure-ad-b2c/samples · GitHub

いや、めんどくさいのが嫌で導入したのにめんどくさいことするの勘弁して欲しいですよね。 B2Cと言うからにはがんばって対応しろよと思うんですが、こればっかりはもうどうにもならないので サンプルどおりに進めることにしました。

ハマりポイント

基本的にこのサンプル通りに進めていけばOKでした。

samples/policies/sign-in-with-apple at master · azure-ad-b2c/samples · GitHub

JWTの部分はこちらのruby版を使用しました。こちらの記事もかなり詳しく Sign in with Apple の導入手順を説明しているのでおすすめです。

What the Heck is Sign In with Apple? | Okta Developer

試しにユーザーフローから実行してみると、ちゃんとAppleIDの認証のページに飛んで認証できました。 ただ認証後に飛ばされるリダイレクトで Bad Request となってしまいました。

アプリで試すと、こちらも認証はOKでリダイレクトでBad Requestとは出ないものの真っ白ページが表示されてWebViewからアプリに応答が戻って来ませんでした。

結論からいうと問題は2つあり、Bad Request は AD B2C のリダイレクトURIの設定、アプリの方の問題は MSAL(MSの認証ライブラリ)の認証メソッドのオプションの問題でした。

AD B2C のリダイレクトURI の設定

AD B2C のホームから「アプリの登録」→ 対象のアプリ → リダイレクト URI のページに飛びます。 そこに developer.apple.com の Service ID のとこで指定した Return URL を設定します。

f:id:kamusoft:20200607132026p:plain

これで Bad Request は出なくなりました。

AD B2C を初めて設定してからかなり時間がたったので、この存在を完全に忘れてました。 最初から順番に設定していく場合は、この問題には遭遇しないかもしれません。

MSAL のオプション

以下の通りです。これだけでApple認証後にアプリに戻って完了できるようになりました。

authResult = await _client.AcquireTokenInteractive(Scopes)
    .WithParentActivityOrWindow(UIParent)
    .WithAccount(GetAccountByPolicy(acounts, PolicySignUpSignIn))
    .WithPrompt(Prompt.SelectAccount)
    .WithUseEmbeddedWebView(true) // これ
    .ExecuteAsync();

WithUseEmbeddedWebViewの指定は、公式サンプルの方にはその記述はなかったのですが

GitHub - Azure-Samples/active-directory-b2c-xamarin-native: This is a simple Xamarin Forms app showcasing how to use MSAL to authenticate users via Azure Active Directory B2C, and access a Web API with the resulting tokens.

こちらの記事のサンプルにありました。

Implementing MSAL + AAD B2C in Xamarin – 6 Tips, Tricks and Facts – Dylan Berry

しかし MSAL 破壊的変更が毎回多すぎる…

追記1

上記の対応は Sign in with Apple では有効ですが、今度は Google のこの問題に対応できなくなってしまうという…

https://docs.microsoft.com/ja-jp/azure/active-directory/develop/msal-net-aad-b2c-considerations#google-auth-and-embedded-webview

Google OAuth は、AD B2C の新しい b2clogin.com ドメインだと埋め込みWebViewでは機能しないということなので、Apple使うとGoogleが死ぬという問題が 発生してしまいます。

これは一旦、旧 microsoftonline.com を使い続け、廃止される2020年末までに対策を考えるしかないかも知れません。 ただ、Appleの方の問題はMSALの問題っぽいので、ちょっとフォークしていろいろ試してみようと思います。

しかし、結構クリティカルな問題なのに、Web上ではほとんど情報が無い。多分、AD B2C をアプリ認証に使ってるのは世界で私一人なんだと思います笑

追記2

iOS 13の不具合だったようで、普通に既出でした。13.4以降だとシステムブラウザ利用しても通りました。 これで、Googleの問題も解消できます。

github.com

AzureADチームは重複issueにも関わらずかなり素早く対応してくれてありがたかったです。 しばらくは使い続けようと思いました笑

その他所見

Sign in with Apple に使う AppID に関してですが、アプリ本体のAppIDを使うのが一般的だと思いますが、 これにグループ用のAppIdを指定することも可能で、そうすることで複数のアプリで同一の認証で通るようになるようです、多分。 個別のアプリのAppIDを指定した場合は、それぞれのアプリで別の認証になるのだと思います。これも推測ですが笑

ただし、グループ用に新規に作ったAppIDにはアプリの実体がないため、認証の際に表示されるアイコンがデフォルトのものになってしまいます。 一方アプリのIDを使えば公開されているアイコンが表示されます。 この辺ゴリ押しするからにはAppleも何とかしろよと思いますが…

公式のフォーラムにも意見は上がってますね。

Custom icon in web login |Apple Developer Forums

おわりに

結果として Azure AD B2C でも Sign in with Apple は利用可能でした。 ただ今回みたいに絶対に対応しないといけないことに対して公式サポートがないのは痛すぎるなという感想です。

認証サービス選びはくれぐれも慎重に!