WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

ASP.NET Core でのデータ保護キーの管理

by WebSurfer 2024年4月25日 19:11

ASP.NET Core Web アプリを IIS でホストするためにアプリケーションプールを作成する際、以下の画像のように[.NET CLR バージョン]を[マネージド コードなし]にするとデータ保護キーがリサイクルで失われ、ブラウザから有効な認証クッキー/チケットが送られてきても復号できないので認証に失敗するという話を書きます。(元の話は「ASP.NET COREで、Cookie認証が維持できない」です)

アプリケーションプールの追加

ASP.NET Core アプリのデータ保護キーの管理は、Microsoft のドキュメント「ASP.NET Core でのデータ保護のキー管理と有効期間」によると、アプリにより運用環境が検出され、以下のいずれかになるそうです。

  1. アプリが Azure Apps でホストされている場合、キーは %HOME%ASP.NET_DataProtection-Keys フォルダーに保持されます。
  2. ユーザー プロファイルを使用できる場合、キーは %LOCALAPPDATA%\ASP.NET\DataProtection-Keys フォルダーに保持されます。
  3. アプリが IIS でホストされている場合、HKLM レジストリ内の、ワーカー プロセス アカウントにのみ ACL が設定されている特別なレジストリ キーにキーが保持されます。
  4. これらの条件のいずれにも該当しない場合、キーは現在のプロセスの外部には保持されません。 プロセスがシャットダウンすると、生成されたキーはすべて失われます。

(ご参考までに、.NET Framework 版の ASP.NET Web アプリの場合は web.config に配置する machineKey 要素 (ASP.NET 設定スキーマ) に従ったデータ保護キーの管理を行い、デフォルト AutoGenerate, IsolateApps の場合はレジストリにデータ保護キーが保持されます)

ということで、IIS でホストされる場合は上の 3 つ目に該当することになり、データ保護キーはレジストリに保存されリサイクルで失われことはない・・・はずですが、上の画像のように[.NET CLR バージョン]を[マネージド コードなし]に設定してアプリケーションプールを作成するとデータ保護キーはレジストリに保存されないようでリサイクルで失われます。なので、リサイクル後はブラウザから有効な認証クッキー/チケットが送られてきても認証に失敗します。(自分の Windows 10 Pro の開発マシンのローカル IIS 10.0 での検証結果です。Windows Server の IIS では少し様子が違うという話がありますが、リサイクルでキーが失われる問題はあるそうです)

知ってました? 自分は知らなかったです。(汗) ASP.NET Core Identity のデフォルトの認証チケットの有効期限は 5 分ですし、短い方がセキュリティ的に望ましいし、短くしていると気が付かないかもしれません。(言い訳)

Microsoft のドキュメント「ASP.NET Core モジュールと IIS の詳細な構成」の「IIS サイトを作成する」のセクションに、

"6. [アプリケーション プールの編集] ウィンドウで、 [.NET CLR バージョン] を [マネージド コードなし] に設定します。"

と書いてあり、 [マネージド コードなし] に設定するのは必須ではないものの推奨されています。ということは、Microsoft の推奨に従ってアプリケーションプールを設定すると、リサイクルで認証が維持できないということになります。

解決策は、アプリケーションプールの追加を行う際、下の画像のように[.NET CLR バージョン]を[.Net CLR バージョン v4.0.30319]にしておくことです。(注: いろいろ試した結果から分かったことで Microsoft の公式ドキュメントに書いてあることではありません)

.NET CLR バージョンの設定

Micosoft の推奨に従って [.NET CLR バージョン] を [マネージド コードなし] に設定したいということであれば、設定を[.Net CLR バージョン v4.0.30319]のままにしておいて一度ホストする ASP.NET Core アプリを動かした後で、 [マネージド コードなし] に設定します。

とにかく一度[.NET CLR バージョン]を[.Net CLR バージョン v4.0.30319]にしてアプリを動かせば、レジストリにデータ保護キーが保持されるようなって、リサイクルでデータ保護キーが失われることはなくなるようです。理由は不明です。

バグっぽいのでそのうち修正されるかもしれませんが、修正されたことがはっきりするまでは要注意だと思います。

なお、アプリケーションプールの名前はデータ保護キーの保存先のレジストリと関連付けられるようですので注意してください。そして、そのレジストリはアプリケーションプールを削除しても残るようです。レジストリにデータ保護キーを保存できるように設定したアプリケーションプールを一旦削除してから、同じ名前で [.NET CLR バージョン] を [マネージド コードなし] にしてアプリケーションプールを作成しても、リサイクル後の認証は維持できます。

また、上に紹介した Stckoverflow の質問者さんの環境では、[ユーザープロファイルの読み込み]の設定が True / False で違いがあって、True の場合は問題ないとのことです。自分が試した限りでは True でも False でも関係なくダメで、リサイクル後の認証は維持できませんでした。想像ですが、上に書いた 2 つ目の条件の「ユーザー プロファイルを使用できる場合」に該当していたのではないかと思われます。

以下にどのように検証したかを備忘録として残しておきます。環境は上にも書きましたが、自分の Windows 10 Pro の開発マシンのローカル IIS 10.0 です。アプリは Microsoft のドキュメント「ASP.NET Core Identity を使用せずに cookie 認証を使用する」から入手した .NET 6.0 の Razor Pages アプリに少し手を加えたものです。

(1) C:\WebSites2019 というフォルダ下に AspNetCoreCookieAuth2 という名前のフォルダを作成。

(2) IIS Manager を起動して AspNetCoreCookieAuthTest という名前でアプリケーションプールを新たに作成。その際[アプリケーション プールの編集]ウィンドウで[.NET CLR バージョン]を[マネージド コードなし]に設定。その他はデフォルトのままとしておきます。結果は以下の画像の通りです。

アプリケーションプール詳細

(3) IIS Manager のサイトの追加で、上記 (1) のフォルダをサイトに設定。サイト名は AspNetCoreCookieAuth2 とし、アプリケーションプールは上記 (2) で新たに作成したものを設定。

サイトの設定

サイトバインド設定は、種類: http, IP アドレス: 未使用の IP アドレスすべて, ポート: 80, ホスト名: www.aspnetcorecookieauth2.com とします。

サイトバインド

(4) hosts ファイルに 127.0.0.1 www.aspnetcorecookieauth2.com を追加します。

(5) ダウンロードした .NET 6.0 のサンプル Razor Pages プロジェクトを Visual Studio 2022 で開いて以下の��うに手を加えます。

  • 永続化(認証クッキーを HDD/SSD に保存)するため既存の Login.cshtml.cs の IsPersistent = true, のコメントアウトを解除
  • 認証チケットの有効期限を 1 週間に設定するため Program.cs の .AddCookie のオプションで有効期限を指定
  • 暗号化されている認証チケットを復号し、認証チケットの有効期限の日時を取得するページ AuthExpireCheck を追加。cshtml.cs のコードは以下の通りです(cshtml のコードは省略します)
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Principal;

namespace CookieSample.Pages
{
    public class AuthExpireCheckModel : PageModel
    {
        private readonly CookieAuthenticationOptions _options;

        public AuthExpireCheckModel(IOptions<CookieAuthenticationOptions> options)
        {
            _options = options.Value;
        }

        public string Message { get; set; }

        public void OnGet()
        {
            try
            {
                // デフォルトのクッキー名は ASP.NET Core Identity の
                // .AspNetCore.Identity.Application とは異なり
                // .AspNetCore.Cookies となっているので注意
                string cookie = Request.Cookies[".AspNetCore.Cookies"];
                if (!string.IsNullOrEmpty(cookie))
                {
                    IDataProtectionProvider provider = _options.DataProtectionProvider;
                    IDataProtector protector = provider.CreateProtector(
                        // CookieAuthenticationMiddleware のフルネーム
                        "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
                        // クッキー名 .AspNetCore.Cookies から .AspNetCore.
                        // を削除した文字列
                        "Cookies",
                        // .NET Framework 版は "v1"、Core 版は "v2"
                        "v2");

                    // 認証クッキーから暗号化された認証チケットを復号
                    TicketDataFormat format = new TicketDataFormat(protector);
                    AuthenticationTicket authTicket = format.Unprotect(cookie);

                    // ユーザー名を取得
                    ClaimsPrincipal principal = authTicket.Principal;
                    IIdentity identity = principal.Identity;
                    string userName = identity.Name;

                    // 認証チケットの有効期限の日時を取得
                    AuthenticationProperties property = authTicket.Properties;
                    DateTimeOffset? expiersUtc = property.ExpiresUtc;

                    // ExpiresUtc と現在の時刻を比較して期限切れか否かを判定
                    if (expiersUtc.Value < DateTimeOffset.UtcNow)
                    {
                        Message = $"{userName} 認証チケット期限切れ {expiersUtc.Value}";
                    }
                    else
                    {
                        Message = $"{userName} 認証チケット期限内 {expiersUtc.Value}";
                    }
                }
                else
                {
                    Message = "認証クッキーがサーバーに送信されてきていません。";
                }
            }
            catch (CryptographicException ex)
            {
                Message = $"CryptographicException: {ex.Message}";
            }
            catch (Exception ex)
            {
                Message = $"Exception: {ex.Message}";
            }
        }
    }
}

(6) Microsoft のチュートリアル「IIS に ASP.NET Core アプリを発行する」の「アプリを発行および配置する」セクションに従って、上のステップで作成した C:\WebSites2019\AspNetCoreCookieAuth2 フォルダにプロジェクトを発行。インプロセスホスティングになります (Microsoft 推奨)。

インプロセスホスティング

(7) ブラウザからサイトにアクセスしてログインしてから上の (5) で追加した AuthExpireCheck ページを要求すると以下の通りとなります。一旦ブラウザを閉じて、再度立ち上げ AuthExpireCheck ページを要求しても認証は維持されます。(IsPersistent = true としたことによる Set-Cookie の expires 属性は有効に機能していることが確認できる)

認証は維持されている

(8) IIS Manager を操作してアプリケーションプルをリサイクルし、ブラウザから AuthExpireCheck ページを要求すると以下の通りとなります。ブラウザからは上の (7) の操作で入手した認証クッキーをサーバーに送信していますが、リサイクルによってデータ保護キーが失われているのでユーザー認証に失敗しています。

認証されてない

Tags: , , , , ,

CORE

ASP.NET Web API と JWT (CORE)

by WebSurfer 2020年2月11日 18:48

ASP.NET Core 3.1 Web API でトークンベース認証を実装してアクセス制限し、ユーザー認証に ASP.NET Core Identity のユーザー情報を利用する方法を書きます。

結果の表示

.NET Framework Web API の場合は、先の記事「ASP.NET Web API の認証」で書きましたように、Visua Studio のテンプレートを使って認証を「個別のユーザーアカウントカウント」として自動生成すればデフォルトでトークンベースの認証が実装され、認証のためのユーザー情報のストアには ASP.NET Identity が使用されます。

Core Web API では自力での実装が必要になります。テンプレートで認証に「個別のユーザーアカウントカウント」を選択すると「クラウドの既存のユーザーストアに接続する」しか選べません。なので、認証なしの状態から Core 2 からサポートされたという JSON Web Token (JWT) を使った認証を実装することにします。

基本的な方法は Auth0 というサイトのブログの記事「ASP.NET Core 2.0 アプリケーションを JWT でセキュアする」(以下「Auth0 の記事」と書きます)に書いてあるのでそれを見れば済む話なのですが、リンク切れになったりすると困るので要点およびその記事には書いてないことを備忘録として残しておきます。

(1) プロジェクトの作成

元になる ASP.NET Core 3.1 Web API アプリのプロジェクトは Visual Studio Community 2019 のテンプレートで自動生成されたものを使います。以下の画像を見てください。認証は「なし」にしておきます。

プロジェクトの作成

テンプレートで自動生成したプロジェクトにはサンプルのコントローラ WeatherForecastController が実装されていて、Visual Studio からプロジェクトを実行([デバッグ(D)]⇒[デバッグなしで開始(H)])すると JSON 文字列が返ってきます。

そのアクションメソッド Get() に JWT ベースの認証を実装します(即ち、トークンが無いとアクセス拒否するようにします)。

(2) NuGet パッケージのインストール

下の画像の赤枠で囲んだ Microsoft.AspNetCore.Authentication.JwtBearer を NuGet からインストールします。青枠で囲んだものは、下に述べたユーザー認証に ASP.NET Core Identity のユーザー情報を利用する場合に必要になります。

NuGet パッケージのインストール

(3) JWT 認証スキーマを登録

自動生成された Startup.cs のコードの ConfigureServices メソッドで、AddAuthentication メソッドを使って JWT 認証スキーマを登録します。コードは Auth0 の記事のものをそのままコピペすれば OK です。using 句の追加を忘れないようにしてください。

さらに、認証を有効にするため Configure メソッドに app.UseAuthentication(); を追加します。既存のコードの app.UseAuthorization(); の前にする必要があるので注意してください。

具体的には以下のコードで「JWT ベースの認証を行うため追加」とコメントしたコードを追加します。

// ・・・前略・・・

// JWT ベースの認証を行うため追加
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

//・・・中略・・・

  public void ConfigureServices(IServiceCollection services)
  {
    // JWT ベースの認証を行うため追加
    services.AddAuthentication(
        JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
          options.TokenValidationParameters = 
            new TokenValidationParameters
            {
              ValidateIssuer = true,
              ValidateAudience = true,
              ValidateLifetime = true,
              ValidateIssuerSigningKey = true,
              ValidIssuer = Configuration["Jwt:Issuer"],
              ValidAudience = Configuration["Jwt:Issuer"],
              IssuerSigningKey = new SymmetricSecurityKey(
                  Encoding.UTF8.GetBytes(
                      Configuration["Jwt:Key"]))
            };
        });

    services.AddControllers();
  }

  public void Configure(IApplicationBuilder app, 
            IWebHostEnvironment env)
  {
    // ・・・中略・・・

    // JWT ベースの認証を行うため追加
    app.UseAuthentication();

    app.UseAuthorization();

    //・・・後略・・・

(4) Key と Issuer を appsettings.json に登録

上の (3) コードでは Key と Issuer を appsettings.json ファイルより取得するようにしていますので、以下のように "Jwt" 要素を追加します。

{

  ・・・中略・・・

  "AllowedHosts": "*",
  "Jwt": {
    "Key": "veryVerySecretKey",
    "Issuer": "https://localhost:44330"
  }
}

既存の "AllowedHosts": "*" の後にカンマ , を追加するのを忘れないようにしてください。Key はパスワードのようなもので任意の文字列を設定できます(16 文字以上にしないとエラーになるようです)。Issuer はサービスを行う URL にします。

(5) [Authorize] 属性を付与

自動生成された WeatherForecastController コントローラの Get() メソッドに [Authorize] 属性を付与します。using Microsoft.AspNetCore.Authorization; の追加を忘れないようにしてください。

ここまでの設定で JWT トークンベースのアクセス制限の実装は完了しており、トークンなしで WeatherForecastController コントローラの Get() メソッドを要求すると HTTP 401 Unauthorized 応答が返ってくるはずです。

(6) トークンを取得する API を実装

ユーザーの ID とパスワードを送信してトークンを取得する API を実装します。基本的には Auth0 の記事のコントローラ TokenController の通りですが、それを拡張してユーザー情報を既存の ASP.NET Core Identity のデータベースから取得して認証を行うようにしてみました。

コントローラ TokenController のコードは以下の通りです。UserManager<IdentityUser> オブジェクトへの参照を DI によって取得し、それを使って既存の ASP.NET Core Identity から情報を取得してユーザー認証に用いています。

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
// 以下を追加
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using Microsoft.AspNetCore.Identity;

namespace WebApiJwtIdentity.Controllers
{
  [Route("api/[controller]")]
  [ApiController]
  public class TokenController : ControllerBase
  {
    private readonly IConfiguration _config;
    private readonly UserManager<IdentityUser> _userManager;

    public TokenController(IConfiguration config, 
            UserManager<IdentityUser> userManager)
    {
      _config = config;
      _userManager = userManager;
    }

    [AllowAnonymous]
    [HttpPost]
    public async Task<IActionResult> CreateToken(
                                        LoginModel login)
    {
      string id = login.Username;
      string pw = login.Password;
      IActionResult response = Unauthorized();
      var user = await _userManager.FindByNameAsync(id);
      if (user != null && 
          await _userManager.CheckPasswordAsync(user, pw))
      {
        var tokenString = BuildToken();
        response = Ok(new { token = tokenString });
      }

      return response;
    }

    private string BuildToken()
    {
      var key = new SymmetricSecurityKey(
          Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
      var creds = new SigningCredentials(
          key, SecurityAlgorithms.HmacSha256);

      var token = new JwtSecurityToken(
        _config["Jwt:Issuer"],
        _config["Jwt:Issuer"],
        expires: DateTime.Now.AddMinutes(30),
        signingCredentials: creds);

      return new JwtSecurityTokenHandler().
                            WriteToken(token);
    }
  }

  public class LoginModel
  {
    public string Username { get; set; }
    public string Password { get; set; }
  }
}

上記のコード以外にも以下の追加が必要です。

(a) 上の (2) の画像で青枠で囲んだ NuGet パッケージのインストール。

(b) IdentityDbContext を継承した ApplicationDbContext クラスを追加。Data フォルダを作ってそれにクラスファイルとして実装します。

using System;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace WebApiJwtIdentity.Data
{
    public class ApplicationDbContext : IdentityDbContext
    {
        public ApplicationDbContext(
            DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
    }
}

(c) appsettings.json に ASP.NET Core Identity が使う既存の SQL Server DB への接続文字列を追加。

(d) Startup.cs に以下を追加。

// 追加
using Microsoft.AspNetCore.Identity;
using WebApiJwtIdentity.Data;
using Microsoft.EntityFrameworkCore;

public void ConfigureServices(IServiceCollection services)
{
  // 追加
  services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(
      Configuration.GetConnectionString(
        "MvcCoreIdentityContextConnection")));

  // 追加
  services.AddDefaultIdentity<IdentityUser>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

(7) 検証用 Home/Index ページを追加

以下は必須ではないですが、検証用の Home/Index ページを追加し、そこから jQuery ajax を使ってトークンの取得と認証が期待通りとなるかを確認してみます。

View のコードは以下のようになります。下のコードの Username と Password には "***" ではなくて有効な文字列を設定してください。このページを使って確認した結果が一番上の画像です。

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Index</title>
  <script src="~/Scripts/jquery.js"></script>
  <script type="text/javascript">
  //<![CDATA[
    var tokenKey = 'accessToken';

    function getToken() {            
      var obj = { Username : "***", Password : "***" };
      var jsonString = JSON.stringify(obj);
      $.ajax({
        type: "POST",
        url: "/api/token",
        data: jsonString,
        contentType: "application/json; charset=utf-8",
        success: function (data, textStatus, jqXHR) {
          sessionStorage.setItem(tokenKey, data.token);
        },
        error: function (jqXHR, textStatus, errorThrown) {
          $('#output').empty();
          $('#output').text('textStatus: ' + textStatus +
              ', errorThrown: ' + errorThrown);
        }
      });
    }

    function weatherForecast() {
      var token = sessionStorage.getItem(tokenKey);
      var headers = {};
      if (token) {
        headers.Authorization = 'Bearer ' + token;
      }

      $.ajax({
        type: "GET",
        url: "/WeatherForecast",
        headers: headers,
        cache: false,
        success: function (data, textStatus, jqXHR) {
          $('#output').empty();
          $.each(data, function (key, val) {
            var day = new Date(val.date);
            var dateString = day.getFullYear() + "年" +
                (day.getMonth() + 1) + "月" +
                day.getDate() + "日";
            $('#output').append(
              '<p>' + dateString + ' / ' +
              val.temperatureC + ' / ' +
              val.temperatureF + ' / ' +
              val.summary + '</p >');
          });                    
        },
        error: function (jqXHR, textStatus, errorThrown) {
          $('#output').empty();
          $('#output').text('textStatus: ' + textStatus +
              ', errorThrown: ' + errorThrown);
        }
      });
    }

  //]]>
  </script>
</head >
<body>
    <h3>Web API から jQuery ajax を使ってデータの取得</h3>

    <input type="button" id="Button1" 
      value="WeatherForecast" onclick="weatherForecast();" />
    <input type="button" id="Button2" 
      value="Get Token" onclick="getToken();" />

    <hr />
    <p>結果の表示:</p>
    <div id="output"></div>
</body>
</html >

なお、元々のプロジェクトの設定が Web API 用ですので、そのままでは MVC 用の Controller と View は動きませんので注意してください。以下の設定が必要になります。

(a) Startup.cs で MVC 用のサービスの追加、静的ファイルの利用を可能にすること、ルーティングのためのマップ設定。

(b) launchSettings.json で "launchUrl" の "weatherforecast" を "home/index" に変更。

(c) jQuery を利用するので wwwroot/Script/jQuery.js を追加。

Tags: , , ,

Web API

ASP.NET MVC に Web API 追加

by WebSurfer 2018年12月22日 18:04

既存の ASP.MET MVC5 アプリに Web API 2.2 のコントローラを追加してみました。その方法を備忘録として書いておきます。需要はないかもしれませんが。(笑)

MVC + Web API プロジェクト

既存の MVC5 アプリは VS2015 のテンプレートで生成した .NET Framework v4.6.1 ベースの単独 MVC プロジェクトで、ASP.NET Identity を利用してクッキーベースの認証を行っています。

その既存の MVC5 アプリに Web API の機能を追加するのですが、Web API の認証は既存のクッキーベースとするのではなく、Web API で推奨されているトークンベースとします。その際、既存の MVC5 アプリが持つ ASP.NET Identity からユーザー情報を得てトークン認証を行うようにします。

基本的には、下の画像のように VS2015 の Web API テンプレートを使ってプロジェクトを新規に作り、それから必要な部分を既存の MVC プロジェクトに追加していくという感じです。

Web API プロジェクトの作成

Web API プロジェクトを作った後、以下の手順で、既存の MVC プロジェクトに必要なパッケージ、コードを追加します。

(1) SSL 有効化

開発環境でも SSL 通信下で検証ができるように、先の記事「IIS Express で SSL 通信」に従って IIS Express で SSL 通信を利用できるように設定します。

(2) NuGet パッケージのインストール

既存の MVC プロジェクトに Web API 関係の NuGet パッケージをインストールします。必要なパッケージは、Web API テンプレートで自動生成されたプロジェクトの NuGet パッケージの管理画面で「インストール済み」の WebApi 関係のパッケージを表示すると分かります。自分の環境では以下の 6 つでした。

  • Microsoft.AspNet.WebApi
  • Microsoft.AspNet.WebApi.Client
  • Microsoft.AspNet.WebApi.Core
  • Microsoft.AspNet.WebApi.WebHost
  • Microsoft.AspNet.WebApi.Owin
  • Microsoft.AspNet.WebApi.HelpPage

Microsoft.AspNet.WebApi を選んでインストールすると自動的に Client, Core, WebHost もインストールされます。Owin, HelpPage はその後で追加インストールしました。

(3) RequireHttpsAttribute の追加

SSL 通信を強制するためのフィルター RequireHttpsAttribute を追加します。(基本的な動作には影響ないのですが、実装しておいた方がよさそうですので)

まず、MVC 側ですが、MVC 5.2 以降であれば System.Web.Mvc 名前空間に RequireHttpsAttribute Class が用意されていますので、それを FilterConfig.cs で以下のように追加します。

public static void RegisterGlobalFilters(
                GlobalFilterCollection filters)
{
    filters.Add(new HandleErrorAttribute());

    // これを追加
    filters.Add(new RequireHttpsAttribute());
}

Web API 側では上記のフィルターは使えないので、カスタムフィルターを作ってそれを使うことになります。(MVC 用と Web API 用とではフィルターは違うので注意)

カスタムフィルターは、Microsoft の文書 Secure a Web API with Individual Accounts and Local Login in ASP.NET Web API 2.2 からリンクが張ってある GitHub のページ からダウンロードできるプロジェクトの Filters フォルダにサンプルがありましたので、それを借用しました。

そのカスタムフィルター RequireHttpsAttribute.cs のコードをそのまま載せておきます。(名前空間は自分のプロジェクトに合わせて変更しました)

using System;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace Mvc5.Filters
{
  public class RequireHttpsAttribute : 
                            AuthorizationFilterAttribute
  {
    public int Port { get; set; }

    public RequireHttpsAttribute()
    {
      Port = 443;
    }

    public override void OnAuthorization(
                           HttpActionContext actionContext)
    {
      var request = actionContext.Request;

      if (request.RequestUri.Scheme != Uri.UriSchemeHttps)
      {
        var response = new HttpResponseMessage();

        if (request.Method == HttpMethod.Get || 
            request.Method == HttpMethod.Head)
        {
          var uri = new UriBuilder(request.RequestUri);
          uri.Scheme = Uri.UriSchemeHttps;
          uri.Port = this.Port;

          response.StatusCode = HttpStatusCode.Found;
          response.Headers.Location = uri.Uri;
        }
        else
        {
          response.StatusCode = HttpStatusCode.Forbidden;
        }

        actionContext.Response = response;
      }
      else
      {
        base.OnAuthorization(actionContext);
      }
    }
  }
}

このカスタムフィルターを有効にする方法は下の「(4) WebApiConfig.cs の追加」のセクションを見てください。

(4) WebApiConfig.cs の追加

Web API プロジェクトから WebApiConfig.cs をコピーして MVC プロジェクトの App_Start フォルダにコピーします。名前空間は自分のプロジェクトに合わせて変更してください。

Register メソッドに、上の「(3) RequireHttpsAttribute の追加」のセクションで用意した Web API 用のカスタムフィルターを有効化するため、以下のコードを追加します。

public static void Register(HttpConfiguration config)
{
  // ・・・中略・・・

  // カスタム RequireHttpsAttribute フィルターを追加
  config.Filters.Add(new Mvc5.Filters.RequireHttpsAttribute());
}

その後、WebApiConfig.Register を Global.asax の Application_Start メソッドに登録します。

protected void Application_Start()
{
    // ・・・中略・・・

    // これを追加
    GlobalConfiguration.Configure(WebApiConfig.Register);
}

(5) UseOAuthBearerTokens 追加

トークン認証を有効にするため、Startup.Auth.cs の ConfigureAuth メソッドに以下のコードを追加します。コードは Web API テンプレートで作成したプロジェクトにありますので、それをコピーして修正すればいいです。

public partial class Startup
{
  // 追加
  public static OAuthAuthorizationServerOptions 
                          OAuthOptions { get; private set; }

  // 追加
  public static string PublicClientId { get; private set; }

  public void ConfigureAuth(IAppBuilder app)
  {

    // ・・・中略・・・

    // 追加
    PublicClientId = "self";
    OAuthOptions = new OAuthAuthorizationServerOptions
    {
      TokenEndpointPath = new PathString("/Token"),
      Provider = new ApplicationOAuthProvider(PublicClientId),

      // 不要と思われるのでコメントアウト(説明下記)
      //AuthorizeEndpointPath = 
      //        new PathString("/api/Account/ExternalLogin"),

      // デフォルトで 20 分。MVC 側に合わせて 14 日に設定
      AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),

      // SSL 強制のため false に設定
      AllowInsecureHttp = false
    };
    app.UseOAuthBearerTokens(OAuthOptions);

    // ・・・後略・・・

AllowInsecureHttp プロパティは SSL 通信強制のため false に設定してください。

ApplicationOAuthProvider は Web API テンプレートで作成したプロジェクトの Providers フォルダにあるものをコピーして使います。詳しくは下の「(6) ApplicationOAuthProvider 追加」セクションを見てください。

AuthorizeEndpointPath プロパティについては、stackoverflow の記事 What is AuthorizeEndpointPath? に説明があります。

ユーザーがクレデンシャルを入力してトークンを得るという条件に限定すれば、AuthorizeEndpointPath プロパティの設定はコメントアウトしても問題なさそうです。(それで 100% 問題ないと言い切れる自信はないですが)

(6) ApplicationOAuthProvider 追加

Web API テンプレートで作成したプロジェクトの Providers フォルダにある ApplicationOAuthProvider.cs をフォルダごと MVC プロジェクトにコピーします。名前空間は自分のプロジェクトに合わせて変更してください。

GrantResourceOwnerCredentials メソッドの中で使用されている第 2 引数に string を持つ GenerateUserIdentityAsync メソッドは、Web API プロジェクトの IdentityModel.cs に定義されているものをコピーして、MVC プロジェクトの IdentityModel.cs にペーストしてください。

このプロバイダは、OWIN ミドルウェアのプラグインとして、OWIN ミドルウェアで発生するイベントを処理するためのものだそうです。詳しくは Microsoft の文書 Secure a Web API with Individual Accounts and Local Login in ASP.NET Web API 2.2 の Configuring the Authorization Server のセクションを見てください。

(7) トークン取得・削除のスクリプト

トークン取得・削除のスクリプトは以下のコードのようにします。

var tokenKey = 'accessToken';

function getToken() {
    var email = document.getElementById("email").value;
    var password = document.getElementById("password").value;

    var loginData = {
        grant_type: 'password',
        username: email,
        password: password
    };

    $.ajax({
        type: "POST",
        url: "/Token",
        data: loginData,
        success: function (data) {
            sessionStorage.setItem(tokenKey, data.access_token);
        },
        error: function (jqXHR, textStatus, errorThrown) {
            //・・・中略・・・
        }
    });
}

function removeToken() {
    sessionStorage.removeItem(tokenKey);
}

以上で MVC 側はクッキーベースで、Web API 側はトークンベースで独立して認証が働きます。それを可能にしているのは WebApiConfig.cs に含まれている以下の 2 行です。

config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(
                    OAuthDefaults.AuthenticationType));

この 2 行をコメントアウトすると Web API 側もクッキーベースの認証となります。なお、その際 Web API に匿名アクセスすると 401 応答ではなく、以下のように 200 応答となりますので注意してください。

クッキー認証の場合の 200 応答

Tags:

Web API

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar