ASP.NET Web API 2 の認証フィルター
作成者: Mike Wasson
認証フィルターは、HTTP 要求を認証するコンポーネントです。 Web API 2 と MVC 5 はどちらも認証フィルターをサポートしていますが、フィルター インターフェイスの名前付け規則では若干異なります。 このトピックでは、Web API 認証フィルターについて説明します。
認証フィルターを使用すると、個々のコントローラーまたはアクションに認証スキームを設定できます。 認証スキームを設定することで、アプリはさまざまな HTTP リソースに対して異なる認証メカニズムをサポートできます。
この記事では、https://github.com/aspnet/samples の基本認証サンプルのコードについて説明します。 このサンプルは、HTTP 基本アクセス認証スキーム (RFC 2617) を実装する認証フィルターを示しています。 フィルターは、IdentityBasicAuthenticationAttribute
という名前のクラスに実装されます。 サンプルのコードをすべて表示するのではなく、認証フィルターを記述する方法を説明する部分のみを表示します。
認証フィルターの設定
他のフィルターと同様に、認証フィルターは、コントローラーごと、アクションごと、またはすべての Web API コントローラーにグローバルに適用できます。
コントローラーに認証フィルターを適用するには、コントローラー クラスを filter 属性で装飾します。 次のコードでは、コントローラー クラスに [IdentityBasicAuthentication]
フィルターを設定します。これにより、コントローラーのすべてのアクションに対して基本認証が有効になります。
[IdentityBasicAuthentication] // Enable Basic authentication for this controller.
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
public IHttpActionResult Get() { . . . }
public IHttpActionResult Post() { . . . }
}
1 つのアクションにフィルターを適用するには、そのアクションをフィルターで装飾します。 次のコードは、コントローラーの Post
メソッドに [IdentityBasicAuthentication]
フィルターを設定します。
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
public IHttpActionResult Get() { . . . }
[IdentityBasicAuthentication] // Enable Basic authentication for this action.
public IHttpActionResult Post() { . . . }
}
すべての Web API コントローラーにフィルターを適用するには、GlobalConfiguration.Filters に追加します。
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new IdentityBasicAuthenticationAttribute());
// Other configuration code not shown...
}
}
Web API 認証フィルターの実装
Web API では、認証フィルターによって System.Web.Http.Filters.IAuthenticationFilter インターフェイスが実装されます。 また、属性として適用するには、System.Attribute から継承する必要があります。
IAuthenticationFilter インターフェイスには、次の 2 つのメソッドがあります。
- AuthenticateAsync は、要求に資格情報が存在する場合は、その資格情報を検証することで要求を認証します。
- ChallengeAsync は、必要に応じて HTTP 応答に認証チャレンジを追加します。
これらの方法は、RFC 2612 および RFC 2617 で定義されている認証フローに対応しています。
- クライアントは Authorization ヘッダーで資格情報を送信します。 これは通常、クライアントがサーバーから 401 (Unauthorized) 応答を受け取った後に発生します。 ただし、クライアントは 401 を受け取った後だけでなく、どのような要求でも資格情報を送信できます。
- サーバーが資格情報を受け入れない場合は、401 (Unauthorized) 応答を返します。 応答には、1 つ以上のチャレンジを含む Www-Authenticate ヘッダーが含まれています。 各チャレンジは、サーバーによって認識される認証スキームを指定します。
また、サーバーは匿名要求から 401 を返すこともできます。 実際には、通常、認証プロセスの開始方法は次のとおりです。
- クライアントは匿名要求を送信します。
- サーバーは 401 を返します。
- クライアントは資格情報を使用して要求を再送信します。
このフローには、認証と承認の両方の手順が含まれます。
- 認証は、クライアントの ID を証明します。
- 承認は、クライアントが特定のリソースにアクセスできるかどうかを決定します。
Web API では、認証フィルターは認証を処理しますが、承認は処理しません。 承認は、承認フィルターまたはコントローラー アクション内で行う必要があります。
Web API 2 パイプラインのフローを次に示します。
- アクションを呼び出す前に、Web API はそのアクションの認証フィルターの一覧を作成します。 これには、アクション スコープ、コントローラー スコープ、グローバル スコープを含むフィルターが含まれます。
- Web API は、リスト内のすべてのフィルターで AuthenticateAsync を呼び出します。 各フィルターは、要求内の資格情報を検証できます。 いずれかのフィルターで資格情報を正常に検証した場合、フィルターは IPrincipal を作成して要求にアタッチします。 また、フィルターは、この時点でエラーをトリガーすることもできます。 その場合、残りのパイプラインは実行されません。
- エラーがないと仮定すると、要求は残りのパイプラインを通過します。
- 最後に、Web API は、すべての認証フィルターの ChallengeAsync メソッドを呼び出します。 フィルターでは、必要に応じて、このメソッドを使用して応答にチャレンジを追加します。 通常 (常にではありません)、401 エラーに応答して発生する可能性があります。
次の図は、考えられる 2 つのケースを示しています。 1 つ目のケースでは、認証フィルターは要求を正常に認証し、承認フィルターは要求を承認し、コントローラー アクションは 200 (OK) を返します。
2 つ目のケースでは、認証フィルターは要求を認証しますが、承認フィルターは 401 (Unauthorized) を返します。 この場合、コントローラー アクションは呼び出されません。 認証フィルターは、応答に Www-Authenticate ヘッダーを追加します。
その他の組み合わせも可能です。たとえば、コントローラー アクションで匿名要求が許可されている場合は、認証フィルターがあっても承認フィルターがない可能性があります。
AuthenticateAsync メソッドの実装
AuthenticateAsync メソッドは、要求の認証を試みます。 このメソッド シグネチャを次に示します。
Task AuthenticateAsync(
HttpAuthenticationContext context,
CancellationToken cancellationToken
)
AuthenticateAsync メソッドは、次のいずれかを実行する必要があります。
- Nothing (no-op)。
- IPrincipal を作成し、要求に対して設定します。
- エラー結果を設定します。
オプション (1) は、要求にフィルターが認識する資格情報がなかったことを意味します。 オプション (2) は、フィルターが要求を正常に認証したことを意味します。 オプション (3) は、要求に無効な資格情報 (間違ったパスワードなど) が含まれており、エラー応答がトリガーされたことを意味します。
AuthenticateAsync を実装するための一般的な概要を次に示します。
- 要求内で資格情報を検索します。
- 資格情報がない場合は、何もせずに (no-op) を返します。
- 資格情報があっても、フィルターが認証スキームを認識しない場合は、何もせずに (no-op) を返します。 パイプライン内の別のフィルターがスキームを理解できる可能性があります。
- フィルターで認識される資格情報がある場合は、認証を試みます。
- 資格情報が正しくない場合は、
context.ErrorResult
を設定して 401 を返します。 - 資格情報が有効な場合は、IPrincipal を作成して
context.Principal
を設定します。
次のコードは、基本認証サンプルの AuthenticateAsync メソッドを示しています。 コメントは各ステップを示します。 このコードには、資格情報のない Authorization ヘッダー、正しくない資格情報、不適切なユーザー名/パスワードなど、いくつかの種類のエラーが示されています。
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
// 1. Look for credentials in the request.
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;
// 2. If there are no credentials, do nothing.
if (authorization == null)
{
return;
}
// 3. If there are credentials but the filter does not recognize the
// authentication scheme, do nothing.
if (authorization.Scheme != "Basic")
{
return;
}
// 4. If there are credentials that the filter understands, try to validate them.
// 5. If the credentials are bad, set the error result.
if (String.IsNullOrEmpty(authorization.Parameter))
{
context.ErrorResult = new AuthenticationFailureResult("Missing credentials", request);
return;
}
Tuple<string, string> userNameAndPassword = ExtractUserNameAndPassword(authorization.Parameter);
if (userNameAndPassword == null)
{
context.ErrorResult = new AuthenticationFailureResult("Invalid credentials", request);
}
string userName = userNameAndPassword.Item1;
string password = userNameAndPassword.Item2;
IPrincipal principal = await AuthenticateAsync(userName, password, cancellationToken);
if (principal == null)
{
context.ErrorResult = new AuthenticationFailureResult("Invalid username or password", request);
}
// 6. If the credentials are valid, set principal.
else
{
context.Principal = principal;
}
}
エラー結果の設定
資格情報が無効な場合、フィルターは context.ErrorResult
をエラー応答を作成する IHttpActionResult に設定する必要があります。 IHttpActionResult の詳細については、「Web API 2 のアクションの結果」を参照してください。
基本認証のサンプルには、この目的に適した AuthenticationFailureResult
クラスが含まれています。
public class AuthenticationFailureResult : IHttpActionResult
{
public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request)
{
ReasonPhrase = reasonPhrase;
Request = request;
}
public string ReasonPhrase { get; private set; }
public HttpRequestMessage Request { get; private set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
response.RequestMessage = Request;
response.ReasonPhrase = ReasonPhrase;
return response;
}
}
ChallengeAsync の実装
ChallengeAsync メソッドの目的は、必要に応じて応答に認証チャレンジを追加することです。 このメソッド シグネチャを次に示します。
Task ChallengeAsync(
HttpAuthenticationChallengeContext context,
CancellationToken cancellationToken
)
このメソッドは、要求パイプライン内のすべての認証フィルターで呼び出されます。
ChallengeAsync は HTTP 応答が作成される前に呼び出され、場合によってはコントローラー アクションが実行される前にも呼び出されることを理解しておくことが重要です。 ChallengeAsync が呼び出されると、context.Result
には IHttpActionResult が含まれ、後で HTTP 応答を作成するために使用されます。 そのため、ChallengeAsync が呼び出されたとき、HTTP 応答についてはまだ何も知りません。 ChallengeAsync メソッドは、context.Result
の元の値を新しい IHttpActionResult に置き換える必要があります。 この IHttpActionResult は、元の context.Result
を折り返す必要があります。
元の IHttpActionResult を内部結果、新しい IHttpActionResult を外部結果と呼びます。 外部結果では、次の操作を行う必要があります。
- 内部結果を呼び出して HTTP 応答を作成します。
- 結果を確認します。
- 必要に応じて、応答に認証チャレンジを追加します。
次の例は、基本認証のサンプルから取得したものです。 外部結果の IHttpActionResult を定義します。
public class AddChallengeOnUnauthorizedResult : IHttpActionResult
{
public AddChallengeOnUnauthorizedResult(AuthenticationHeaderValue challenge, IHttpActionResult innerResult)
{
Challenge = challenge;
InnerResult = innerResult;
}
public AuthenticationHeaderValue Challenge { get; private set; }
public IHttpActionResult InnerResult { get; private set; }
public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
HttpResponseMessage response = await InnerResult.ExecuteAsync(cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
// Only add one challenge per authentication scheme.
if (!response.Headers.WwwAuthenticate.Any((h) => h.Scheme == Challenge.Scheme))
{
response.Headers.WwwAuthenticate.Add(Challenge);
}
}
return response;
}
}
この InnerResult
プロパティには、内部 IHttpActionResult が格納されます。 この Challenge
プロパティは、Www-Authentication ヘッダーを表します。 ExecuteAsync は最初に HTTP 応答を作成するために InnerResult.ExecuteAsync
を呼び出し、必要に応じてチャレンジを追加します。
チャレンジを追加する前に、応答コードを確認してください。 ほとんどの認証スキームでは、次に示すように、応答が 401 の場合にのみチャレンジが追加されます。 ただし、一部の認証スキームでは、成功応答にチャレンジが追加されます。 例として、Negotiate (RFC 4559) を参照してください。
AddChallengeOnUnauthorizedResult
クラスを考えると、ChallengeAsync の実際のコードはシンプルです。 結果を作成して context.Result
にアタッチするだけです。
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
var challenge = new AuthenticationHeaderValue("Basic");
context.Result = new AddChallengeOnUnauthorizedResult(challenge, context.Result);
return Task.FromResult(0);
}
注: 基本認証のサンプルでは、このロジックを拡張メソッドに配置することで、少し抽象化します。
認証フィルターとホスト レベル認証の組み合わせ
"ホスト レベルの認証" とは、要求が Web API フレームワークに到達する前に、ホスト (IIS など) によって実行される認証のことを指します。
多くの場合、アプリケーションの残りの部分でホスト レベルの認証を有効にしても、Web API コントローラーでは無効にすることができます。 たとえば、一般的なシナリオでは、ホスト レベルでフォーム認証を有効にし、Web API ではトークン ベースの認証を使用するということがあります。
Web API パイプライン内でホスト レベルの認証を無効にするには、構成で config.SuppressHostPrincipal()
を呼び出します。 これにより、Web API は、Web API パイプラインに入る要求から IPrincipal を削除します。 実質的には、要求を "認証解除" します。
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.SuppressHostPrincipal();
// Other configuration code not shown...
}
}
その他のリソース
ASP.NET Web API セキュリティ フィルター (MSDN マガジン)