ぷろじぇくと、みすじら。

AuthenticationHandlerで処理する方法をアクションごとに変更する

Created at:

ASP.NET Core MVCでは認証は基本的にフィルターではなく、認証ハンドラーを認証スキーム名で登録しておき、使用したい認証スキームをAuthorize属性などで指定するという形になっています(デフォルトという指定もできます)。

今回やりたいこととしては「認証機構は基本的に一つで、あるアクションの時だけ認証中に追加で特殊な処理を行いたい」というパターンです。例えばコントローラーに共通のAuthorizeを付けているが、一部特殊なアクションでは追加の処理をしたいというケースです。

難しいところ

そもそも認証ハンドラーはASP.NET Coreの一部であってMVCとは切り離されたものなので、操作可能なものは主にHttpContextとなります。つまりASP.NET Core MVCが認識しているアクションであるとかコントローラーであるとかを取り出すことは難しいため、アクションに付けた属性を引いてくるといったことはできないという悩みがあります。

うまくいかないパターン

そこで最初に考えたのは、二つの認証スキームとして登録してそれぞれで利用する認証スキームを変更するという方法です。

まず次のようなオプションを持つAuthenticationHandlerを用意します。

public class CustomAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
public bool EnableNanika { get; set; }
}

public class CustomAuthenticationHandler : AuthenticationHandler<CustomAuthenticationSchemeOptions>
{
public CustomAuthenticationHandler(IOptionsMonitor<CustomAuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (Options.EnableNanika)
{
// 一部で必要な特殊な処理...
}
return Task.FromResult(AuthenticateResult.Fail("Unauthorized"));
}

protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.Headers["WWW-Authenticate"] = "Nanika";
return base.HandleChallengeAsync(properties);
}
}

このハンドラーをStartupで二つの認証スキームとして登録します。

services.AddAuthentication()
.AddScheme<CustomAuthenticationSchemeOptions, CustomAuthenticationHandler>("Custom1", options =>
{
options.EnableNanika = false;
})
.AddScheme<CustomAuthenticationSchemeOptions, CustomAuthenticationHandler>("Custom2", options =>
{
options.EnableNanika = true;
});

コントローラーにAuthorize属性を付け、その際にコントローラーとアクションで認証スキームを別々に設定します。

[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = "Custom1")]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
[Authorize(AuthenticationSchemes = "Custom2")]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}

これでどうかと思ったのですが、この指定で実際に動かすとコントローラーの指定の方が勝つことになります。当然ですがコントローラーにAuthorizeを付けずにアクションごと個別につけると期待通りに動作します。

うまくいかないパターン(2)

認証はフィルターとしても実装できるので(Authorizeも実体はMVCのフィルターで、中で認証ハンドラーを呼び出している)、それをかけてみるのはどうかという風になります。

public class CustomAuthorize : Attribute, IAsyncAuthorizationFilter
{
public bool EnableNanika { get; set; }

public Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
// ここで認証処理をあれこれやる
return Task.CompletedTask;
}
}

この場合にはコントローラーとアクション両方に指定すると二つのフィルターが呼ばれることになる(=二回処理が走る)ので都合が悪い感じになります。それに加えてAuthorize属性と同じようなことを書いてあげる必要があり面倒です。

解決策: フィルターを組み合わせるパターン

認証はフィルターとして実装できるという点を利用して、認証自体は行わずリクエストのデータにフラグを立てておき、認証ハンドラーで取り出して何とかする方法があります。その方法であれば 自前のフィルター(ASP.NET Core MVC) → 自前の認証ハンドラー (ASP.NET Core) という形にできます。

まずアクションにつけるフィルターを作ります。

public class UseCustomAuthenticationWithNanikaAttribute : Attribute, IAsyncAuthorizationFilter, IOrderedFilter
{
public int Order => int.MinValue;

public Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
context.HttpContext.Features.Set<IUseCustomAuthenticationWithNanikaFeature>(new UseCustomAuthenticationWithNanikaFeature
{
Enable = true
});
return Task.CompletedTask;
}
}

public interface IUseCustomAuthenticationWithNanikaFeature
{
bool Enable { get; set; }
}
public class UseCustomAuthenticationWithNanikaFeature : IUseCustomAuthenticationWithNanikaFeature
{
public bool Enable { get; set; }
}

このフィルターはOnAuthorizationAsyncでHttpContextのFeatures(リクエスト単位の機能を入れておけるもの)にフラグ情報を持つFeatureを保存します。IOrderedFilterを実装しているのはAuthorize属性より先に来てほしいためです。

次に認証ハンドラーを作ります。このハンドラーの中でFeaturesから先ほどのフィルターでつけていたFeatureを引っ張り出します。

public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public CustomAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var enableNanika = Context.Features.Get<IUseCustomAuthenticationWithNanikaFeature>()?.Enable ?? false;
if (enableNanika)
{
// 一部で必要な特殊な処理...
}

return Task.FromResult(AuthenticateResult.Fail("Unauthorized"));
}

protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.Headers["WWW-Authenticate"] = "Nanika";
return base.HandleChallengeAsync(properties);
}
}

Startupで認証ハンドラーを登録します。この際、AddAuthenticationでデフォルト認証スキームを登録しないように注意します。デフォルトの認証スキームとなるハンドラーは順番が特別なことになるので意図した挙動になりません。

services.AddAuthentication()
.AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>("Custom", options => { });

そしてコントローラーにはAuthorize属性を、特別な処理をしたいアクションにはUseCustomAuthenticationWithNanika属性でフィルターを付けます。

[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = "Custom")]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
[UseCustomAuthenticationWithNanika]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}

// GET api/values/5
[HttpGet("{id}")]
public string Get(int id)
{
return "value";
}
}

これでアクションに属性を指定するだけで認証時の挙動を少し変更するということが可能になります。

U-NEXT TV (第二世代)はAndroidおもちゃではない

Created at:

U-NEXT、9,800円のHDR対応Android TV「U-NEXT TV」。カラオケやYouTubeも - AV Watchという記事を見て「先着100名で、新規にU-NEXT登録した人を対象にU-NEXT TVをプレゼントするキャンペーン」とあったので登録してみたところ無事届きました。というわけでAndroidとして遊べないかなというところに関してのお話です。

製品仕様はキャンペーンサイトに書いてあり、Android 7.0であることが明記されています。

U-NEXT TV (2nd Gen)

端末自体はHuaweiです。この手のセットトップボックスではHuawei製はよくあるようです(たしかauやdocomoのものもそうだったはず)。

OS

さて、Androidに関してですがよく見ると公式サイトの製品仕様にはAndroid 7.0とはあるもののAndroid TVとは書いていないのでもしかしたら…と思っていたのですがセットアップし始めるとやはりAndroid TVではなく単なるAndroidです。

そもそもセットアップステップがAndroid TV風でなくU-NEXTのログインが必須であったり、オンスクリーンキーボードもAndroid TVのそれではない、ホーム画面は独自というあたりでお察しです。

製品仕様によればアプリも利用できることになっているのですがAndroidがベースになっているだけでGoogleサービスが入っていないのでGoogle Playは使えません。「U-NEXTサービス、HDMI-CEC対応、Player for YouTube、Androidなど」とあるので使えるのかと思いきや。

YouTubeを観れそうな雰囲気ですがGoogle Play入ってないのにどうやって?と思ったらHuaweiの独自アプリでした。oh…。

現状U-NEXT用のアプリストアがあるわけでもないのでプレインストールのアプリのみ利用できます。なお設定のアプリ管理からアプリをアンインストールできるのですが、それらはインストールする口がないので消したら最後です(これはひどい。

参考までにほぼ同型機のdocomoのドコモテレビターミナルは説明書を見てもわかるのですが正真正銘のAndroid TVでロゴも表記されています。

adbを使う

とはいえAndroidなのでUSBデバッグとかできればなんかいろいろできるかもということで探すとちゃんとUSBデバッグはあります。

「アプリ」→「端末設定」→「情報」→「開発者オプション」→「USBデバッグ」で有効になるので有効にして、USBケーブルをつなぐと許可するかどうかのダイアログが表示されます。ちなみにUSBケーブルはType-Aなので注意が必要です。

接続後はいつものようにadbでコマンドを実行できshellにも入れます。そこで早速apkをインストールしてみようとすると次のようなエラーが発生しました。

adb -s 0123456789 install "app-release.apk"
adb: failed to install app-release.apk: Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Verify signature failed]

どうも署名チェックのようなものがあり、そこで引っかかっているようです。

提供元不明のアプリのインストールを許可する必要があるのか?と思ったので設定画面を開いてセキュリティから許可してみましたがダメです。Webを検索してもそれらしい情報はないので端末固有でロックをかけていそうです。うーん。

なお、設定画面は am コマンドを使えば開けます。

HWM380:/ $ am start -a android.intent.action.MAIN -n com.android.settings/.Settings

ちなみにバージョン情報はこんな感じです。

もう少し探ってみたところ /system/etc/security/apk_sign_white_list.xml というファイルがあり、署名のホワイトリストのようなものだったのでユーザーが任意にインストールするのは無理そうですね(HuaweiやU-NEXTだけが含まれている)。

一応 pm list packageの結果、パッケージの一覧はこんな感じでした。

まとめ

残念ながらAndroid端末としてはいじりがいがないのでそこを期待してはダメそうです。とはいえ特定サービス向けの装置なのでそこはしょうがないかなーという気もします。

U-NEXTのSTBとしてはきびきび動いたりするので普通に使う分には割とよいのではないかとは思いますが、実際お金を出す場合半額の4,980円でもどうかなー…って感じがしますね。リテラシがなくてつなげば使えるみたいなのを求めていない限り、正直縛りのないAndroid TVであるAirStick 4Kの方がよさそう。

そうそう、ログイン必須なので退会すると完全に使い物にならなくなります。