ASP.NET Web API 2.2 において個別のアカウントおよびローカル ログインで Web API を保護する

作成者: Mike Wasson

サンプル アプリをダウンロードする

このトピックでは、OAuth2 を使用してメンバーシップ データベースに対して認証し、 Web API をセキュリティで保護する方法について説明します。

チュートリアルで使用するソフトウェアのバージョン

Visual Studio 2013 では、Web API プロジェクト テンプレートに認証用の 3 つのオプションがあります:

  • 個人アカウント。 アプリはメンバーシップ データベースを使用します。
  • 組織アカウント。 ユーザーは、Azure Active Directory、Office 365、またはオンプレミスの Active Directory 資格情報を使用してサインインします。
  • Windows 認証です。 このオプションはイントラネット アプリケーションを対象としており、Windows 認証 IIS モジュールを使用します。

これらのオプションの詳細については、「Visual Studio 2013 での ASP.NET Web プロジェクトの作成」を参照してください。

個々のアカウントには、ユーザーがログインするための 2 つの方法があります:

  • ローカル ログイン。 ユーザーはユーザー名とパスワードを入力し、サイトに登録します。 アプリは、パスワード ハッシュをメンバーシップ データベースに格納します。 ユーザーがログインすると、ASP.NET Identity システムによってパスワードが検証されます。
  • ソーシャル ログイン。 ユーザーは、Facebook、Microsoft、Google などの外部サービスでサインインします。 アプリは、メンバーシップ データベースにユーザーのエントリを作成しますが、資格情報は保存しません。 ユーザーは、外部サービスにサインインして認証します。

この記事では、ローカル ログイン シナリオについて説明します。 ローカル ログインとソーシャル ログインの両方で、Web API は OAuth2 を使用して要求を認証します。 ただし、ローカル ログインとソーシャル ログインでは資格情報フローが異なります。

この記事では、ユーザーがログインし、認証された AJAX 呼び出しを Web API に送信できる簡単なアプリについて説明します。 サンプル コードは、ここからダウンロードできます。 Readme では、Visual Studio でサンプルを最初から作成する方法について説明します。

Image of sample form

サンプル アプリでは、データ バインディングに Knockout.js を使用し、AJAX 要求を送信するために jQuery を使用します。 この記事では AJAX 呼び出しに焦点を当てるので、Knockout.js を理解している必要はありません。

順に次の内容について説明します:

  • アプリがクライアント側で実行していること。
  • サーバーで何が起こっているか。
  • 中間の HTTP トラフィック。

まず、OAuth2 の用語をいくつか定義する必要があります。

  • リソース。 保護できるデータの一部。
  • リソース サーバー。 リソースをホストするサーバー。
  • リソース所有者。 リソースにアクセスするためのアクセス許可を付与できるエンティティ。 (通常はユーザー。)
  • クライアント: リソースへのアクセスを必要とするアプリ。 この記事では、クライアントは Web ブラウザーです。
  • アクセス トークン。 リソースへのアクセスを許可するトークン。
  • ベアラー トークン。 任意のユーザーがトークンを使用できるプロパティを持つ特定の種類のアクセス トークン。 言い換えると、クライアントはベアラー トークンを使用するために暗号化キーやその他のシークレットを必要としません。 そのため、ベアラー トークンは HTTPS 経由でのみ使用し、有効期限は比較的短くする必要があります。
  • 承認サーバー。 アクセス トークンを提供するサーバー。

アプリケーションは、承認サーバーとリソース サーバーの両方として機能できます。 Web API プロジェクト テンプレートは、このパターンに従います。

ローカル ログイン資格情報フロー

ローカル ログインの場合、Web API は OAuth2 で定義されたリソース所有者パスワード フローを使用します。

  1. ユーザーがクライアントに名前とパスワードを入力します。
  2. クライアントは、これらの資格情報を承認サーバーに送信します。
  3. 承認サーバーは資格情報を認証し、アクセス トークンを返します。
  4. 保護されたリソースにアクセスするために、クライアントは HTTP 要求の Authorization ヘッダーにアクセス トークンを含めます。

Diagram of local login credential flow

Web API プロジェクト テンプレートで [個々のアカウント] を選択すると、プロジェクトには、ユーザーの資格情報を検証してトークンを発行する承認サーバーが含まれます。 次の図は、Web API コンポーネントに関して同じ資格情報フローを示しています。

Diagram when individual accounts is selected in the Web A P I

このシナリオでは、Web API コントローラーはリソース サーバーとして機能します。 認証フィルターはアクセス トークンを検証し、[Authorize] 属性を使用してリソースを保護します。 コントローラーまたはアクションに [Authorize] 属性がある場合、そのコントローラーまたはアクションに対するすべての要求を認証する必要があります。 それ以外の場合、承認は拒否され、Web API は 401 (未承認) エラーを返します。

承認サーバーと認証フィルターの両方で、OAuth2 の詳細を処理する OWIN ミドルウェア コンポーネントが呼び出されます。 このチュートリアルの後半で設計について詳しく説明します。

承認されていない要求の送信

開始するには、アプリを実行し、[API の呼び出し] ボタンをクリックします。 要求が完了すると、[結果] ボックスにエラー メッセージが表示されます。 これは、要求にアクセス トークンが含まれていないため、要求が承認されていないためです。

Image of result error message

[API の呼び出し] ボタンは、Web API コントローラー アクションを呼び出す ~/api/values に AJAX 要求を送信します。 AJAX 要求を送信する JavaScript コードのセクションを次に示します。 サンプル アプリでは、すべての JavaScript アプリ コードが Scripts\app.js ファイルにあります。

// If we already have a bearer token, set the Authorization header.
var token = sessionStorage.getItem(tokenKey);
var headers = {};
if (token) {
    headers.Authorization = 'Bearer ' + token;
}

$.ajax({
    type: 'GET',
    url: 'api/values/1',
    headers: headers
}).done(function (data) {
    self.result(data);
}).fail(showError);

ユーザーがログインするまで、ベアラー トークンがないため、要求に Authorization ヘッダーはありません。 これにより、要求から 401 エラーが返されます。

HTTP 要求を次に示します。 (ここでは、Fiddler を使用して HTTP トラフィックをキャプチャします。)

GET https://localhost:44305/api/values HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Accept-Language: en-US,en;q=0.5
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/

HTTP 応答:

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
WWW-Authenticate: Bearer
Date: Tue, 30 Sep 2014 21:54:43 GMT
Content-Length: 61

{"Message":"Authorization has been denied for this request."}

応答に、チャレンジが Bearer に設定された Www-Authenticate ヘッダーが含まれていることに注意してください。 これは、サーバーがベアラー トークンが送られてくると想定していることを示します。

ユーザーを登録する

アプリの [登録] セクションで、メールとパスワードを入力し、[登録] ボタンをクリックします。

このサンプルでは有効なメール アドレスを使用する必要はありませんが、実際のアプリではアドレスが確認されます。 (「ログイン、電子メール確認、パスワード リセットを使用して安全な ASP.NET MVC 5 Web アプリを作成する」を参照してください。)パスワードには、"Password1!" のように、大文字、小文字、数字、英数字以外の文字を使用します。 アプリをシンプルにするために、クライアント側の検証を省略したので、パスワード形式に問題がある場合は、400 (無効な要求) エラーが発生します。

Image of register a user section

[登録] ボタンは、~/api/Account/Register/に POST 要求を送信します。 要求本文は、名前とパスワードを保持する JSON オブジェクトです。 要求を送信する JavaScript コードを次に示します:

var data = {
    Email: self.registerEmail(),
    Password: self.registerPassword(),
    ConfirmPassword: self.registerPassword2()
};

$.ajax({
    type: 'POST',
    url: '/api/Account/Register',
    contentType: 'application/json; charset=utf-8',
    data: JSON.stringify(data)
}).done(function (data) {
    self.result("Done!");
}).fail(showError);

HTTP 要求。ここで、$CREDENTIAL_PLACEHOLDER$ は、パスワード キーと値のペアのプレースホルダーです:

POST https://localhost:44305/api/Account/Register HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 84

{"Email":"alice@example.com",$CREDENTIAL_PLACEHOLDER1$,$CREDENTIAL_PLACEHOLDER2$"}

HTTP 応答:

HTTP/1.1 200 OK
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 00:57:58 GMT
Content-Length: 0

この要求は AccountController クラスによって処理されます。 内部的には、AccountController は ASP.NET Identity を使用してメンバーシップ データベースを管理します。

Visual Studio からアプリをローカルで実行すると、ユーザー アカウントは LocalDB の AspNetUsers テーブルに格納されます。 Visual Studio でテーブルを表示するには、[表示] メニューをクリックし、[サーバー エクスプローラー] を選択し、[データ接続] を展開します。

Image of Data Connections

アクセス トークンを取得する

ここまでは OAuth を実行していませんが、これからアクセス トークンを要求すると、OAuth 承認サーバーが動作することを確認します。 サンプル アプリの [ログイン] 領域で、メール アドレスとパスワードを入力し、[ログイン] をクリックします。

Image of log in section

[ログイン] ボタンは、トークン エンドポイントに要求を送信します。 要求の本文には、次のフォーム URL でエンコードされたデータが含まれています:

  • grant_type: "password"
  • username: <ユーザーのメール アドレス>
  • password: <パスワード>

AJAX 要求を送信する JavaScript コードを次に示します:

var loginData = {
    grant_type: 'password',
    username: self.loginEmail(),
    password: self.loginPassword()
};

$.ajax({
    type: 'POST',
    url: '/Token',
    data: loginData
}).done(function (data) {
    self.user(data.userName);
    // Cache the access token in session storage.
    sessionStorage.setItem(tokenKey, data.access_token);
}).fail(showError);

要求が成功した場合、承認サーバーは応答本文でアクセス トークンを返します。 後で API に要求を送信するときに使用するために、トークンをセッション ストレージに格納していることに注意してください。 一部の形式の認証 (Cookie ベースの認証など) とは異なり、ブラウザーは後続の要求にアクセス トークンを自動的に含めません。 アプリケーションは明示的にこれを行う必要があります。 CSRF の脆弱性が制限されるため、これは良いことです。

HTTP 要求:

POST https://localhost:44305/Token HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 68

grant_type=password&username=alice%40example.com&password=Password1!

要求にユーザーの資格情報が含まれていることを確認してください。 トランスポート層のセキュリティを提供するには、HTTPS を使用する必要があります

HTTP 応答:

HTTP/1.1 200 OK
Content-Length: 669
Content-Type: application/json;charset=UTF-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:22:36 GMT

{
  "access_token":"imSXTs2OqSrGWzsFQhIXziFCO3rF...",
  "token_type":"bearer",
  "expires_in":1209599,
  "userName":"alice@example.com",
  ".issued":"Wed, 01 Oct 2014 01:22:33 GMT",
  ".expires":"Wed, 15 Oct 2014 01:22:33 GMT"
}

読みやすくするために、JSON をインデントし、アクセス トークン (非常に長いため) を切り捨てました。

access_tokentoken_type および expires_in プロパティは、OAuth2 仕様によって定義されます。その他のプロパティ (userName.issued および .expires) は情報提供のみを目的とします。 TokenEndpoint メソッドにこれらの追加プロパティを追加するコードは、/Providers/ApplicationOAuthProvider.cs ファイルにあります。

認証済み要求を送信する

ベアラー トークンが作成されたので、API に対して認証された要求を行うことができます。 これを行うには、要求に Authorization ヘッダーを設定します。 もう一度 [API の呼び出し] ボタンをクリックして、これを確認します。

Image after call A P I button has been clicked

HTTP 要求:

GET https://localhost:44305/api/values/1 HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Authorization: Bearer imSXTs2OqSrGWzsFQhIXziFCO3rF...
X-Requested-With: XMLHttpRequest

HTTP 応答:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:41:29 GMT
Content-Length: 27

"Hello, alice@example.com."

Log Out

ブラウザーは資格情報またはアクセス トークンをキャッシュしないため、ログアウトは単にトークンをセッション ストレージから削除することで "忘れる" ということです:

self.logout = function () {
    sessionStorage.removeItem(tokenKey)
}

個々のアカウント プロジェクト テンプレートについて

ASP.NET Web アプリケーション プロジェクト テンプレートで個々のアカウントを選択すると、プロジェクトには次のものが含まれます:

  • OAuth2 承認サーバー。
  • ユーザー アカウントを管理するための Web API エンドポイント
  • ユーザー アカウントを格納するための EF モデル。

これらの機能を実装する主なアプリケーション クラスを次に示します:

  • AccountController。 ユーザー アカウントを管理するための Web API エンドポイントを提供します。 Register アクションは、このチュートリアルで使用した唯一のアクションです。 クラスの他のメソッドは、パスワードのリセット、ソーシャル ログイン、およびその他の機能をサポートします。
  • ApplicationUser。これは /Models/IdentityModels.cs で定義されています。 このクラスは、メンバーシップ データベース内のユーザー アカウントの EF モデルです。
  • ApplicationUserManager。これは /App_Start/IdentityConfig.cs で定義されています。このクラスは UserManager から派生し、新しいユーザーの作成、パスワードの検証などの操作をユーザー アカウントに対して実行し、データベースへの変更を自動的に保持します。
  • ApplicationOAuthProvider。 このオブジェクトは OWIN ミドルウェアにプラグインし、ミドルウェアによって発生したイベントを処理します。 これは OAuthAuthorizationServerProvider から派生します。

Image of main application classes

承認サーバーを構成する

StartupAuth.cs では、次のコードによって OAuth2 承認サーバーが構成されます。

PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
    TokenEndpointPath = new PathString("/Token"),
    Provider = new ApplicationOAuthProvider(PublicClientId),
    AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
    AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
    // Note: Remove the following line before you deploy to production:
    AllowInsecureHttp = true
};

// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);

TokenEndpointPath プロパティは、承認サーバー エンドポイントへの URL パスです。 これは、ベアラー トークンを取得するためにアプリが使用する URL です。

Provider プロパティは、OWIN ミドルウェアにプラグインし、ミドルウェアによって発生したイベントを処理するプロバイダーを指定します。

アプリがトークンを取得する場合の基本的なフローを次に示します:

  1. アクセス トークンを取得するために、アプリは ~/Token に要求を送信します。
  2. OAuth ミドルウェアはプロバイダーの GrantResourceOwnerCredentials を呼び出します。
  3. プロバイダーは、資格情報を検証し、クレーム ID を作成するために ApplicationUserManager を呼び出します。
  4. 成功した場合、プロバイダーは認証チケットを作成します。これはトークンの生成に使用されます。

Diagram of authorization flow

OAuth ミドルウェアは、ユーザー アカウントについて何も認識しません。 プロバイダーはミドルウェアと ASP.NET Identity の間で通信します。 承認サーバーの実装の詳細については、「OWIN OAuth 2.0 Authorization Server」を参照してください。

ベアラー トークンを使用するように Web API を構成する

WebApiConfig.Register メソッドでは、次のコードによって Web API パイプラインの認証が設定されます:

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

HostAuthenticationFilter クラスは、ベアラー トークンを使用した認証を有効にします。

SuppressDefaultHostAuthentication メソッドは、IIS または OWIN ミドルウェアから送信された要求が Web API パイプラインに到達する前に発生したすべての認証を無視するように Web API に指示します。 そのため、ベアラー トークンを使用する場合にのみ認証するように Web API を制限することができます。

Note

特に、アプリの MVC 部分では、Cookie に資格情報を格納するフォーム認証が使用される場合があります。 Cookie ベースの認証では、CSRF 攻撃を防ぐために偽造防止トークンを使用する必要があります。 Web API には偽造防止トークンをクライアントに送信する便利な方法がないため、これは Web API にとって問題です。 (この問題の詳細な背景については、「Web API での CSRF 攻撃の防止」を参照してください。)SuppressDefaultHostAuthentication を呼び出すと、Cookie に格納されている資格情報からの CSRF 攻撃に対して Web API が脆弱でないことを保証します。

クライアントが保護されたリソースを要求すると、Web API パイプラインで次の処理が行われます:

  1. HostAuthentication フィルターは、OAuth ミドルウェアを呼び出してトークンを検証します。
  2. ミドルウェアは、トークンをクレーム ID に変換します。
  3. この時点で、要求は認証されますが、承認されていません。
  4. 承認フィルターは、クレーム ID を調べます。 クレームがそのリソースに対してユーザーを承認した場合、要求は承認されます。 既定では、[Authorize] 属性は、認証されたすべての要求を承認します。 ただし、ロールまたは他のクレームによって承認することもできます。 詳細については、「Web API での認証と承認」を参照してください。
  5. ここまでの手順が成功した場合、コントローラーは保護されたリソースを返します。 それ以外の場合は、クライアントは 401 (未承認) エラーを受け取ります。

Diagram of when the client requests a protected resource

その他のリソース