kamulog

xamarin.formsのネタなど

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 は利用可能でした。 ただ今回みたいに絶対に対応しないといけないことに対して公式サポートがないのは痛すぎるなという感想です。

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

SettingsView ver.1.1.1 新機能の紹介

ちびちびと development ブランチで大改造してたんですが、ここら辺で一区切りしようとpreとしてリリースすることにしました。 色々と変更がありますが、主な変更をご紹介します。

github.com

CustomCell

SettingsViewのCellのコンテンツ部分をFormsViewで定義できるもので、ViewCellのようなものです。 ViewCellと異なるところは以下の通りです。

  • タッチした時の反応(色の変化)がある
  • 他のSettingsViewのCellと同じような余白がある
    • 無しにもできる
  • Command / LongCommand がある
  • アイコンを設定できる
  • 右端に矢印アイコンを表示できる

これにより、SettingsViewに定義されてない機能のCellを自由に定義することが可能です。 ただNativeCellに比べるとパフォーマンスは落ちると思います。 CustomCellRendererは protected virtual ばっかりにしたので、それを乗っ取れば好きなNativeCellを作れるかも知れません笑

詳細はReadMeを参照してください。

Section Header と Footer には文字列しか指定できませんでしたが、それぞれFormsViewを指定できるようになりました。 これにより、表現の幅がかなり広がったかと思います。

こちらも詳細は ReadMeを参照してください。

TemplateStartIndex

テンプレートの適用を開始するindexを指定できます。 これを使用すると例えばこういう定義の場合

<sv:Section ItemsSource="{Binding CountList}" TemplateStartIndex="1">
    <sv:Section.ItemTemplate>
        <DataTemplate>
            <sv:NumberPickerCell Min="1" Max="999" Number="{Binding}"  />
        </DataTemplate>
    </sv:Section.ItemTemplate>            
    <sv:LabelCell Title="カウント設定" />
</sv:Section>

XAMLの子要素のベタ書きとDataTemplateを混在させることができ、最初にLabelCellが表示され、TemplateStartIndexで指定された1番目からDataTemplateが適用され、ItemsSourceの要素の数だけNumberPickerCellが表示されます。

f:id:kamusoft:20200118215945p:plain

部分的にDataTemplateを適用できるので結構便利かと思います。

Section またぎの D&D

そもそも並べ替え機能とか誰も使ってないかもしれませんが笑、その並べ替え機能は今まで同一セクション内に限定されていましたが、UseDragSortがTrueになっているSectionの間で自由にD&Dで動かせるようになりました。

Cell / Section の部分更新

今まではセル1個の変更だろうが、問答無用でUITableView.ReloadData()やAdapter.NotifyDataSetChanged()を呼び出してたので、動的変更が非常に低パフォーマンスでしたが、それぞれInsertRowsなどの個別更新通知を呼び出すように変更して、そのあたりのパフォーマンスは改善されたと思います。 これに伴い、InsertやDeleteの際にセルがアニメーションするようになりました。 SectionのVisibleの切り替えの際も、個別更新するようにしたので、よりスムーズになりました。

まとめ

セルの個別変更通知の実装や、TemplateStartIndexの導入でかなり既存の部分を書き換えたので、しばらくはPreReleaseでの公開にしたいと思います。プロダクトに導入するのはおすすめできません笑

何かあればIssueで報告していただけると幸いです。

Xamarin.Forms CarouselView を試してみる

この記事は Xamarin Advent Calendar 2019 の 5日目の記事です。

CarouselViewの公式版が試験版で実装されていたので試してみました。

docs.microsoft.com

結果

www.youtube.com

終盤のAndroidのボタンクリックの動きは想定外です笑

下準備

試験版なので AppDelegate.cs / MainActivity.cs で Forms.Initの前に以下のコードが必要です。

Xamarin.Forms.Forms.SetFlags("CarouselView_Experimental");

XAML

XAMLとしては以下な感じで使います。

<CarouselView Grid.Row="0" ItemsSource="{Binding Books}" 
        ItemSizingStrategy="MeasureFirstItem"
        HorizontalScrollBarVisibility="Never"
        PeekAreaInsets="{OnPlatform Android=150,iOS=50}"
        NumberOfSideItems="0"
        Position="{Binding Position.Value}"
        RemainingItemsThreshold="1"
        RemainingItemsThresholdReachedCommand="{Binding BindingContext.MoreCommand,Source={x:Reference me}}"
        VerticalOptions="Center" HorizontalOptions="Center" HeightRequest="350">
    <CarouselView.ItemsLayout>
        <LinearItemsLayout Orientation="Horizontal"
                            ItemSpacing="10"
                            SnapPointsType="MandatorySingle"
                            SnapPointsAlignment="Center" />
    </CarouselView.ItemsLayout>
    <CarouselView.ItemTemplate>
        <DataTemplate x:DataType="model:WebBook">
            <Frame CornerRadius="16" BorderColor="Silver" HasShadow="False" IsClippedToBounds="True">
                <Image Source="{Binding Thumbnail}" Aspect="AspectFill" Margin="-20" />
            </Frame>
        </DataTemplate>
    </CarouselView.ItemTemplate>
</CarouselView>

プロパティ

  • HorizontalScrollBarVisibility
    • スクロールバーの表示有無。Neverで非表示に。
  • PeekAreaInsets
    • 両隣でチラ見せするThickness値。これを設定することで現在のアイテムの両隣のアイテムの一部を表示させることが可能。
    • これはかなり嬉しい機能です。ただAndroidの単位が論理ピクセルじゃなくて物理ピクセルサイズな気がしてならない。
    • なのでAndroidだけ3倍の値にして誤魔化しました。
  • NumberOfSideItems
    • 隣に表示するアイテムの数。1にすると現在のアイテムの両隣に1個ずつの計3つのアイテムが並ぶことになります。
    • PeekAreaInsetsと組み合わせ可能で、組み合わせると計3つと両端に一部を表示させるといった感じになります。
  • Position
    • 現在のアイテムのIndexを参照したり設定したりします。
    • 現時点では初期位置を指定しても動かなかったり、指定位置に動かそうとしても動かなかったり微妙です。
  • RemainingItemsThreshold
    • 残り何個の段階でRemainingItemsThresholdReachedCommandまたは同様の機能のイベントを呼ぶかの閾値
    • 0だと最後が表示された段階で、1だと1つ前のアイテムが表示された段階でCommand/Eventを発火する。
  • RemainingItemsThresholdReachedCommand
    • 最後まで(最後からRemainingItemsThreshold番目まで)表示したら発火するCommand。
    • データの追加読み込みなどで使う。
  • SnapPointsType
    • スクロールした時の動き。
    • MandatorySingleで一回のスワイプで1項目分スクロールして自然にとまる感じの動きになる。
    • これが一番カルーセルっぽいと思います。
  • SnapPointsAlignment
    • スクロールが止まった時の配置
    • Centerがカルーセルとして自然な動きかと思います。

WidthRequestに関して

アイテムのWidthRequestの値はCarouselの特性上無視されるようです。 なのでアスペクト比を考慮する場合はデバイスの幅とかを取得して計算して各プロパティの値を決めていく必要があると思います。

RemainingItemsThresholdReachedCommand の注意事項

AsyncReactiveCommand などの二度押し防止機能付きなら問題ないのですが、そうでない場合は、閾値に達してからスクロールを続ける度にCommandが連続で発火してしまいます。 なので

if (isLoading)
{
    return;
}
isLoading = true;
// 読み込み処理
isLoading = false;

みたいな処理が多分必要になると思います。

ViewModel例

Prism + ReactiveProperty 使ってます。

public class MainPageViewModel:BindableBase,INavigatedAware
{
    public ObservableCollection<WebBook> Books { get; set; }
    public ReactivePropertySlim<int> Position { get; } = new ReactivePropertySlim<int>(0);
    public ReactivePropertySlim<WebBook> CurrentItem { get; } = new ReactivePropertySlim<WebBook>();
    public AsyncReactiveCommand MoreCommand { get; } = new AsyncReactiveCommand();
    public ReactiveCommand PrevCommand { get; } = new ReactiveCommand();
    public ReactiveCommand NextCommand { get; } = new ReactiveCommand();

    int _nextPage = 0;
    IWebApi _webApi;
    public MainPageViewModel(IWebApi webApi)
    {
        _webApi = webApi;

        MoreCommand.Subscribe(async _ =>
        {
            foreach(var book in await GetData())
            {
                Books.Add(book);
            }
        });

        PrevCommand.Subscribe(_ =>
        {
            if(Position.Value > 0)
            {
                Position.Value--;
            }
        });

        NextCommand.Subscribe(_ =>
        {
            if (Position.Value < Books.Count - 1)
            {
                Position.Value++;
            }
        });
    }

    public void OnNavigatedTo(INavigationParameters parameters)
    {
        _ = InitializeData();
    }

    public async Task InitializeData()
    {
        Books = new ObservableCollection<WebBook>(await GetData());
        RaisePropertyChanged(nameof(Books));
        Position.Value = 1;
    }

    public async Task<List<WebBook>> GetData()
    {
        var books = await _webApi.GetByKeyword("Xamarin", 6, _nextPage);
        if(books.Count() < 6)
        {
            _nextPage = 0;
        }
        else
        {
            _nextPage += 6;
        }

        return books.ToList();
    }
}

初期化の段階で ObservableCollection に foreach で Addしていくと、エラーになったり6件あるはずが2件しか表示されなかったりと、うまくいかなかったので、new でListを渡してインスタンスを生成するようにしてます。 MoreCommandの段階では逐次追加でも問題なかったのでforeachの方法で。

Position.Value = 1; で初期位置を2番目にしてるつもりなんですが、動きませんでした笑

GetDataでは最後まで読み込んだら0からにする処理を入れて一応無限スクロールっぽくしています。

まとめ

現時点では間違いなく試験版でした。実践投入はまだ早いかなといったところです。 特にPosition周りとスワイプの挙動が怪しいです。 ただ表示するだけなら現時点でも問題はなさそうです。 プラグイン版には無かった現在アイテム以外のチラ見せとかにも対応しているので、安定したら頼もしいコントロールになると思います。 期待して待ちましょう!

リポジトリ

github.com