web-dev-qa-db-ja.com

C#(Web API)で(ベアラートークンを使用して)認証されたメソッドをテストする適切な方法

使用するためにベアラートークンが存在する必要があるメソッドがたくさんあるWebAPIがあります。これらのメソッドはすべて、ベアラートークンから情報を抽出します。

APIが生成時にベアラートークンを適切に設定しているかどうかをテストしたいと思います。 Microsoft.Owin.Testingフレームワークを使用してテストを記述しています。私は次のようなテストを行っています:

[TestMethod]
public async Task test_Login() 
{
    using (var server = TestServer.Create<Startup>())
    {
        var req = server.CreateRequest("/authtoken");
        req.AddHeader("Content-Type", "application/x-www-form-urlencoded");
        req.And(x => x.Content = new StringContent("grant_type=password&username=test&password=1234", System.Text.Encoding.ASCII));
        var response = await req.GetAsync();

        // Did the request produce a 200 OK response?
        Assert.AreEqual(response.StatusCode, System.Net.HttpStatusCode.OK);

        // Retrieve the content of the response
        string responseBody = await response.Content.ReadAsStringAsync();
        // this uses a custom method for deserializing JSON to a dictionary of objects using JSON.NET
        Dictionary<string, object> responseData = deserializeToDictionary(responseBody); 

        // Did the response come with an access token?
        Assert.IsTrue(responseData.ContainsKey("access_token"));

    }
}

したがって、トークンを表す文字列を取得できます。しかし今、私は実際にそのトークンの内容にアクセスし、特定のクレームが提供されていることを確認したいと思います。

クレームをチェックするために実際の認証済みメソッドで使用するコードは次のようになります。

var identity = (ClaimsIdentity)User.Identity;
IEnumerable<Claim> claims = identity.Claims;

var claimTypes = from x in claims select x.Type;

if (!claimTypes.Contains("customData"))
    throw new InvalidOperationException("Not authorized");

したがって、私が実行できるようにしたいのは、テスト自体の中で、ベアラートークン文字列を提供し、User.Identityオブジェクトを受信するか、他の方法でトークンに含まれるクレームにアクセスすることです。これは、私のメソッドが必要なクレームをトークンに適切に追加しているかどうかをテストする方法です。

「ナイーブ」なアプローチは、与えられたベアラートークン内のすべてのクレームを返すだけのメソッドをAPIに記述することです。しかし、これは不要なはずだと感じています。 ASP.NETは、コントローラーのメソッドが呼び出される前に、指定されたトークンをオブジェクトにデコードしています。テストコードで、同じアクションを自分で複製したいと思います。

これはできますか?もしそうなら、どのように?


編集:私のOWINスタートアップクラスは、認証とトークン生成を処理する、私がコーディングした認証トークンプロバイダーをインスタンス化します。私のスタートアップクラスにはこれがあります:

public void Configuration(IAppBuilder app)
{
    // Setup configuration object
    HttpConfiguration config = new HttpConfiguration();

    // Web API configuration and services
    // Configure Web API to use only bearer token authentication.
    config.SuppressDefaultHostAuthentication();
    config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

    // Web API routes
    config.MapHttpAttributeRoutes();
    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );

    // configure the OAUTH server
    OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
    {
        //AllowInsecureHttp = false,
        AllowInsecureHttp = true, // THIS HAS TO BE CHANGED BEFORE PUBLISHING!

        TokenEndpointPath = new PathString("/authtoken"),
        AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
        Provider = new API.Middleware.MyOAuthProvider()
    };

    // Now we setup the actual OWIN pipeline.

    // setup CORS support
    // in production we will only allow from the correct URLs.
    app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);

    // Token Generation
    app.UseOAuthAuthorizationServer(OAuthServerOptions);
    app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

    // insert actual web API and we're off!
    app.UseWebApi(config);
}

これが私のOAuthプロバイダーからの関連コードです:

public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{

    // Will be used near end of function
    bool isValidUser = false;

    // Simple sanity check: all usernames must begin with a lowercase character
    Match testCheck = Regex.Match(context.UserName, "^[a-z]{1}.+$");
    if (testCheck.Success==false)
    {
        context.SetError("invalid_grant", "Invalid credentials.");
        return;
    }

    string userExtraInfo;
    // Here we check the database for a valid user.
    // If the user is valid, isValidUser will be set to True.
    // Invalid authentications will return null from the method below.
    userExtraInfo = DBAccess.getUserInfo(context.UserName, context.Password);
    if (userExtraInfo != null) isValidUser = true;

    if (!isValidUser)
    {
        context.SetError("invalid_grant", "Invalid credentials.");
        return;
    }

    // The database validated the user. We will include the username in the token.
    string userName = context.UserName;

    // generate a claims object
    var identity = new ClaimsIdentity(context.Options.AuthenticationType);

    // add the username to the token
    identity.AddClaim(new Claim(ClaimTypes.Sid, userName));

    // add the custom data on the user to the token.
    identity.AddClaim(new Claim("customData", userExtraInfo));

    // store token expiry so the consumer can determine expiration time
    DateTime expiresAt = DateTime.Now.Add(context.Options.AccessTokenExpireTimeSpan);
    identity.AddClaim(new Claim("expiry", expiresAt.ToString()));

    // Validate the request and generate a token.
    context.Validated(identity);

}

単体テストでは、customDataクレームが実際に認証トークンに存在することを確認する必要があります。したがって、どのクレームが含まれているかをテストするために提供されたトークンを評価する方法が必要です。


編集2:Katanaのソースコードを調べたり、他の投稿をオンラインで検索したりするのに時間を費やしました。このアプリをIISでホストすることが重要であるように思われるため、SystemWebを使用します。 SystemWebはトークンにマシンキー暗号化を使用しているようです。また、オプションのAccessTokenFormatパラメータがここに関連しているように見えます。

だから今私が疑問に思っているのは、この知識に基づいて自分の「デコーダー」をインスタンス化できるかどうかです。 IISでのみホストすることになると仮定すると、トークンをデコードしてクレームオブジェクトに変換できるデコーダーをインスタンス化できますか?

これに関するドキュメントはまばらで、コードはあちこちにあなたを投げ込んでいるようです。私の頭の中でまっすぐに保つことを試みることがたくさんあります。


編集3:ベアラートークンデシリアライザーと思われるものを含むプロジェクトを見つけました。私はその「API」ライブラリのコードを適応させ、それを使用してAPIによって生成されたトークンを復号化しようとしています。

MicrosoftのPowerShellスクリプトを使用して<machineKey...>値を生成し、それをAPI自体のWeb.configファイルとテストプロジェクトのApp.confgファイルの両方に配置しました。

ただし、トークンはまだ復号化に失敗します。例外がスローされました:System.Security.Cryptography.CryptographicExceptionメッセージ"Error occurred during a cryptographic operation."以下はエラーのスタックトレースです。

at System.Web.Security.Cryptography.HomogenizingCryptoServiceWrapper.HomogenizeErrors(Func`2 func, Byte[] input)
at System.Web.Security.Cryptography.HomogenizingCryptoServiceWrapper.Unprotect(Byte[] protectedData)
at System.Web.Security.MachineKey.Unprotect(ICryptoServiceProvider cryptoServiceProvider, Byte[] protectedData, String[] purposes)
at System.Web.Security.MachineKey.Unprotect(Byte[] protectedData, String[] purposes)
at MyAPI.Tests.BearerTokenAPI.MachineKeyDataProtector.Unprotect(Byte[] protectedData) in D:\Source\MyAPI\MyAPI.WebAPI.Tests\BearerTokenAPI.cs:line 251
at MyAPI.Tests.BearerTokenAPI.SecureDataFormat`1.Unprotect(String protectedText) in D:\Source\MyAPI\MyAPI.WebAPI.Tests\BearerTokenAPI.cs:line 287

この時点で私は困惑しています。 MachineKey値がプロジェクト全体で同じに設定されていると、トークンを復号化できない理由がわかりません。暗号化エラーは意図的に曖昧になっていると思いますが、今どこから始めればよいのかわかりません。

そして、私がやりたかったのは、トークンに単体テストで目的のデータが含まれていることをテストすることだけでした。..:-)

7
fdmillion

私はついに解決策を見つけることができました。 OAuthBearerAuthenticationOptionsメソッドに渡されたUseBearerTokenAuthenticationオブジェクトを公開するパブリック変数をStartupクラスに追加しました。そのオブジェクトから、AccessTokenFormat.Unprotectを呼び出して、復号化されたトークンを取得できます。

また、テストを書き直して、Startupクラスを個別にインスタンス化し、テスト内から値にアクセスできるようにしました。

MachineKeyが機能しない理由、トークンの保護を直接解除できない理由がまだわかりません。 MachineKeyが一致している限り、手動でもトークンを復号化できるはずです。しかし、それが最善の解決策でなくても、少なくともこれはうまくいくようです。

これはおそらくもっときれいに行うことができます。たとえば、Startupクラスは、テスト中に開始されているかどうかを何らかの方法で検出し、オブジェクトをそよ風にぶら下げるのではなく、他の方法でテストクラスに渡すことができます。しかし今のところ、これは私が必要としていたことを正確に実行しているようです。

私のスタートアップクラスは、次のように変数を公開します。

public partial class Startup
{
    public OAuthBearerAuthenticationOptions oabao;

    public void Configuration(IAppBuilder app)
    {

        // repeated code omitted

        // Token Generation
        app.UseOAuthAuthorizationServer(OAuthServerOptions);
        oabao = new OAuthBearerAuthenticationOptions();
        app.UseOAuthBearerAuthentication(oabao);

        // insert actual web API and we're off!
        app.UseWebApi(config);

    }
}

私のテストは次のようになります。

[TestMethod]
public async Task Test_SignIn()
{
    Startup owinStartup = new Startup();
    Action<IAppBuilder> owinStartupAction = new Action<IAppBuilder>(owinStartup.Configuration);

    using (var server = TestServer.Create(owinStartupAction))
    {
        var req = server.CreateRequest("/authtoken");
        req.AddHeader("Content-Type", "application/x-www-form-urlencoded");

        // repeated code omitted

        // Is the access token of an appropriate length?
        string access_token = responseData["access_token"].ToString();
        Assert.IsTrue(access_token.Length > 32);

        AuthenticationTicket token = owinStartup.oabao.AccessTokenFormat.Unprotect(access_token);

        // now I can check whatever I want on the token.
    }
}

うまくいけば、私のすべての努力が、他の誰かが同じようなことをしようとしているのを助けるでしょう。

14
fdmillion