WinHTTP での認証

一部の HTTP サーバーとプロキシでは、インターネット上のリソースへのアクセスを許可する前に認証が必要です。 Microsoft Windows HTTP Services (WinHTTP) 関数は、HTTP セッションのサーバー認証とプロキシ認証をサポートします。

HTTP 認証について

認証が必要な場合、HTTP アプリケーションは状態コード 401 (サーバーに認証が必要) または 407 (プロキシが認証を必要とします) を受け取ります。 状態コードと共に、プロキシまたはサーバーは 1 つ以上の認証ヘッダー (WWW-Authenticate (サーバー認証用) または Proxy-Authenticate (プロキシ認証用) を送信します。

各認証ヘッダーには、サポートされている認証スキームと、Basic および Digest スキームの領域が含まれています。 複数の認証スキームがサポートされている場合、サーバーは複数の認証ヘッダーを返します。 領域の値では大文字と小文字が区別され、同じ資格情報が受け入れられる一連のサーバーまたはプロキシが定義されます。 たとえば、サーバー認証が必要な場合、ヘッダー "WWW-Authenticate: Basic Realm="example" が返される場合があります。 このヘッダーは、"example" ドメインにユーザー資格情報を指定する必要があることを指定します。

HTTP アプリケーションには、サーバーに送信する要求を含む承認ヘッダー フィールドを含めることができます。 承認ヘッダーには、認証スキームと、そのスキームで必要な適切な応答が含まれています。 たとえば、"Authorization: Basic <username:password>" というヘッダーが要求に追加され、クライアントが応答ヘッダー "WWW-Authenticate: Basic Realm="example" を受信した場合にサーバーに送信されます。

Note

ここではプレーン テキストとして表示されますが、ユーザー名とパスワードは実際には base64 でエンコードされます

 

認証スキームには、次の 2 つの一般的な種類があります。

  • 基本認証スキーム。ユーザー名とパスワードがクリア テキストでサーバーに送信されます。

    基本認証スキームは、クライアントが各領域のユーザー名とパスワードを使用して自身を識別する必要があるモデルに基づいています。 サーバーは、有効なユーザー名とパスワードを含む承認ヘッダーを使用して要求が送信された場合にのみ要求を処理します。

  • サーバーが 認証データを使用してクライアントにチャレンジする、Kerberos などのチャレンジ応答スキーム。 クライアントは、ユーザー資格情報を使用してデータを変換し、変換されたデータを認証のためにサーバーに送信します。

    チャレンジ応答スキームにより、より安全な認証が可能になります。 チャレンジ応答スキームでは、ユーザー名とパスワードがネットワーク経由で送信されることはありません。 クライアントがチャレンジ応答スキームを選択すると、サーバーは、そのスキームの 認証データ を含むチャレンジを含む適切な状態コードを返します。 その後、クライアントは、要求されたサービスを取得するための適切な応答を使用して要求を再送信します。 チャレンジ対応スキームでは、完了するために複数の交換が必要な場合があります。

次の表に、WinHTTP でサポートされている認証スキーム、認証の種類、およびスキームの説明を示します。

Scheme Type 説明
Basic (プレーンテキスト) Basic ユーザー名とパスワードを含む base64 でエンコードされた 文字列を使用します。
ダイジェスト チャレンジ応答 nonce (サーバー指定のデータ文字列) 値を使用する場合の課題。 有効な応答には、ユーザー名、パスワード、指定された nonce 値、 HTTP 動詞、および要求された Uniform Resource Identifier (URI) のチェックサムが含まれています。
NTLM チャレンジ応答 ID を証明するために、ユーザー資格情報を使用して 認証データ を変換する必要があります。 NTLM 認証が正しく機能するためには、同じ接続で複数の交換を行う必要があります。 そのため、介在するプロキシがキープアライブ接続をサポートしていない場合は、NTLM 認証を使用できません。 また、キープアライブ セマンティクスを無効にする WINHTTP_DISABLE_KEEP_ALIVE フラグで WinHttpSetOption を使用すると、NTLM 認証も失敗します。
Passport チャレンジ応答 Microsoft Passport 1.4 を使用します。
ネゴシエート チャレンジ応答 サーバーとクライアントの両方が Windows 2000 以降を使用している場合は、Kerberos 認証が使用されます。 それ以外の場合は、NTLM 認証が使用されます。 Kerberos は、Windows 2000 以降のオペレーティング システムで使用でき、NTLM 認証よりもセキュリティが高いと見なされます。 ネゴシエート認証が正しく機能するためには、同じ接続で複数の交換を行う必要があります。 そのため、間に存在するプロキシがキープアライブ接続をサポートしていない場合は、ネゴシエート認証を使用できません。 また、キープアライブ セマンティクスを無効にする WINHTTP_DISABLE_KEEP_ALIVE フラグと共に WinHttpSetOption を使用すると、ネゴシエート認証も失敗します。 ネゴシエート認証スキームは統合Windows 認証と呼ばれることもあります。

 

WinHTTP アプリケーションでの認証

WinHTTP アプリケーション プログラミング インターフェイス (API) には、認証が必要な状況でインターネット リソースにアクセスするために使用される 2 つの関数 ( WinHttpSetCredentialsWinHttpQueryAuthSchemes) が用意されています。

401 または 407 の状態コードで応答を受信すると、 WinHttpQueryAuthSchemes を使用して認証ヘッダーを解析し、サポートされている認証スキームと認証ターゲットを決定できます。 認証ターゲットは、認証を要求するサーバーまたはプロキシです。 WinHttpQueryAuthSchemes は、サーバーによって推奨される認証スキームの基本設定に基づいて、使用可能なスキームから最初の認証スキームも決定します。 認証スキームを選択するためのこの方法は、 RFC 2616 で推奨される動作です。

WinHttpSetCredentials を使用すると、アプリケーションは、ターゲット サーバーまたはプロキシで使用する有効なユーザー名とパスワードと共に使用される認証スキームを指定できます。 資格情報を設定し、要求を再送信すると、必要なヘッダーが生成され、要求に自動的に追加されます。 一部の認証スキームでは複数のトランザクションが必要であるため、 WinHttpSendRequest はエラーを返す可能性があるため、ERROR_WINHTTP_RESEND_REQUEST。 このエラーが発生した場合、アプリケーションは、401 または 407 状態コードを含まない応答が受信されるまで要求を再送信し続ける必要があります。 200 状態コードは、リソースが使用可能であり、要求が成功したことを示します。 返すことができる追加の 状態コードについては、HTTP 状態 コードを参照してください。

要求がサーバーに送信される前に許容される認証スキームと資格情報がわかっている場合、アプリケーションは WinHttpSendRequest を呼び出す前に WinHttpSetCredentials を呼び出すことができます。 この場合、WinHTTP は、サーバーへの最初の要求で資格情報または 認証データ を提供することで、サーバーとの事前認証を試みます。 事前認証により、認証プロセスの交換数が減少し、アプリケーションのパフォーマンスが向上する可能性があります。

事前認証は、次の認証スキームで使用できます。

  • 基本 - 常に可能です。
  • Kerberos への解決をネゴシエートする - 可能性が非常に高い。唯一の例外は、クライアントとドメイン コントローラーの間で時間のずれがオフになっている場合です。
  • (NTLM への解決をネゴシエートする) - 決して不可能です。
  • NTLM - Windows Server 2008 R2 でのみ使用できます。
  • ダイジェスト - 決して不可能です。
  • Passport - 決して不可能です。最初のチャレンジ応答の後、WinHTTP は Cookie を使用して Passport に対する事前認証を行います。

一般的な WinHTTP アプリケーションでは、認証を処理するために次の手順を実行します。

WinHttpSetCredentials によって設定された資格情報は、1 つの要求にのみ使用されます。 WinHTTP では、他の要求で使用する資格情報はキャッシュされません。つまり、複数の要求に応答できるアプリケーションを記述する必要があります。 認証された接続を再利用すると、他の要求にチャレンジできない場合がありますが、コードはいつでも要求に応答できる必要があります。

例: ドキュメントの取得

次のサンプル コードでは、HTTP サーバーから指定したドキュメントの取得を試みます。 認証が必要かどうかを判断するために、状態コードが応答から取得されます。 200 の状態コードが見つかった場合は、ドキュメントを使用できます。 状態コード 401 または 407 が見つかった場合は、ドキュメントを取得する前に認証が必要です。 その他の状態コードの場合は、エラー メッセージが表示されます。 使用可能 な状態コード の一覧については、HTTP 状態コードを参照してください。

#include <windows.h>
#include <winhttp.h>
#include <stdio.h>

#pragma comment(lib, "winhttp.lib")

DWORD ChooseAuthScheme( DWORD dwSupportedSchemes )
{
  //  It is the server's responsibility only to accept 
  //  authentication schemes that provide a sufficient
  //  level of security to protect the servers resources.
  //
  //  The client is also obligated only to use an authentication
  //  scheme that adequately protects its username and password.
  //
  //  Thus, this sample code does not use Basic authentication  
  //  becaus Basic authentication exposes the client's username
  //  and password to anyone monitoring the connection.
  
  if( dwSupportedSchemes & WINHTTP_AUTH_SCHEME_NEGOTIATE )
    return WINHTTP_AUTH_SCHEME_NEGOTIATE;
  else if( dwSupportedSchemes & WINHTTP_AUTH_SCHEME_NTLM )
    return WINHTTP_AUTH_SCHEME_NTLM;
  else if( dwSupportedSchemes & WINHTTP_AUTH_SCHEME_PASSPORT )
    return WINHTTP_AUTH_SCHEME_PASSPORT;
  else if( dwSupportedSchemes & WINHTTP_AUTH_SCHEME_DIGEST )
    return WINHTTP_AUTH_SCHEME_DIGEST;
  else
    return 0;
}

struct SWinHttpSampleGet
{
  LPCWSTR szServer;
  LPCWSTR szPath;
  BOOL fUseSSL;
  LPCWSTR szServerUsername;
  LPCWSTR szServerPassword;
  LPCWSTR szProxyUsername;
  LPCWSTR szProxyPassword;
};

void WinHttpAuthSample( IN SWinHttpSampleGet *pGetRequest )
{
  DWORD dwStatusCode = 0;
  DWORD dwSupportedSchemes;
  DWORD dwFirstScheme;
  DWORD dwSelectedScheme;
  DWORD dwTarget;
  DWORD dwLastStatus = 0;
  DWORD dwSize = sizeof(DWORD);
  BOOL  bResults = FALSE;
  BOOL  bDone = FALSE;

  DWORD dwProxyAuthScheme = 0;
  HINTERNET  hSession = NULL, 
             hConnect = NULL,
             hRequest = NULL;

  // Use WinHttpOpen to obtain a session handle.
  hSession = WinHttpOpen( L"WinHTTP Example/1.0",  
                          WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                          WINHTTP_NO_PROXY_NAME, 
                          WINHTTP_NO_PROXY_BYPASS, 0 );

  INTERNET_PORT nPort = ( pGetRequest->fUseSSL ) ? 
                        INTERNET_DEFAULT_HTTPS_PORT  :
                        INTERNET_DEFAULT_HTTP_PORT;

  // Specify an HTTP server.
  if( hSession )
    hConnect = WinHttpConnect( hSession, 
                               pGetRequest->szServer, 
                               nPort, 0 );

  // Create an HTTP request handle.
  if( hConnect )
    hRequest = WinHttpOpenRequest( hConnect, 
                                   L"GET", 
                                   pGetRequest->szPath,
                                   NULL, 
                                   WINHTTP_NO_REFERER, 
                                   WINHTTP_DEFAULT_ACCEPT_TYPES,
                                   ( pGetRequest->fUseSSL ) ? 
                                       WINHTTP_FLAG_SECURE : 0 );

  // Continue to send a request until status code 
  // is not 401 or 407.
  if( hRequest == NULL )
    bDone = TRUE;

  while( !bDone )
  {
    //  If a proxy authentication challenge was responded to, reset
    //  those credentials before each SendRequest, because the proxy  
    //  may require re-authentication after responding to a 401 or  
    //  to a redirect. If you don't, you can get into a 
    //  407-401-407-401- loop.
    if( dwProxyAuthScheme != 0 )
      bResults = WinHttpSetCredentials( hRequest, 
                                        WINHTTP_AUTH_TARGET_PROXY, 
                                        dwProxyAuthScheme, 
                                        pGetRequest->szProxyUsername,
                                        pGetRequest->szProxyPassword,
                                        NULL );
    // Send a request.
    bResults = WinHttpSendRequest( hRequest,
                                   WINHTTP_NO_ADDITIONAL_HEADERS,
                                   0,
                                   WINHTTP_NO_REQUEST_DATA,
                                   0, 
                                   0, 
                                   0 );

    // End the request.
    if( bResults )
      bResults = WinHttpReceiveResponse( hRequest, NULL );

    // Resend the request in case of 
    // ERROR_WINHTTP_RESEND_REQUEST error.
    if( !bResults && GetLastError( ) == ERROR_WINHTTP_RESEND_REQUEST)
        continue;

    // Check the status code.
    if( bResults ) 
      bResults = WinHttpQueryHeaders( hRequest, 
                                      WINHTTP_QUERY_STATUS_CODE |
                                      WINHTTP_QUERY_FLAG_NUMBER,
                                      NULL, 
                                      &dwStatusCode, 
                                      &dwSize, 
                                      NULL );

    if( bResults )
    {
      switch( dwStatusCode )
      {
        case 200: 
          // The resource was successfully retrieved.
          // You can use WinHttpReadData to read the 
          // contents of the server's response.
          printf( "The resource was successfully retrieved.\n" );
          bDone = TRUE;
          break;

        case 401:
          // The server requires authentication.
          printf(" The server requires authentication. Sending credentials...\n" );

          // Obtain the supported and preferred schemes.
          bResults = WinHttpQueryAuthSchemes( hRequest, 
                                              &dwSupportedSchemes, 
                                              &dwFirstScheme, 
                                              &dwTarget );

          // Set the credentials before resending the request.
          if( bResults )
          {
            dwSelectedScheme = ChooseAuthScheme( dwSupportedSchemes);

            if( dwSelectedScheme == 0 )
              bDone = TRUE;
            else
              bResults = WinHttpSetCredentials( hRequest, 
                                        dwTarget, 
                                        dwSelectedScheme,
                                        pGetRequest->szServerUsername,
                                        pGetRequest->szServerPassword,
                                        NULL );
          }

          // If the same credentials are requested twice, abort the
          // request.  For simplicity, this sample does not check
          // for a repeated sequence of status codes.
          if( dwLastStatus == 401 )
            bDone = TRUE;

          break;

        case 407:
          // The proxy requires authentication.
          printf( "The proxy requires authentication.  Sending credentials...\n" );

          // Obtain the supported and preferred schemes.
          bResults = WinHttpQueryAuthSchemes( hRequest, 
                                              &dwSupportedSchemes, 
                                              &dwFirstScheme, 
                                              &dwTarget );

          // Set the credentials before resending the request.
          if( bResults )
            dwProxyAuthScheme = ChooseAuthScheme(dwSupportedSchemes);

          // If the same credentials are requested twice, abort the
          // request.  For simplicity, this sample does not check 
          // for a repeated sequence of status codes.
          if( dwLastStatus == 407 )
            bDone = TRUE;
          break;

        default:
          // The status code does not indicate success.
          printf("Error. Status code %d returned.\n", dwStatusCode);
          bDone = TRUE;
      }
    }

    // Keep track of the last status code.
    dwLastStatus = dwStatusCode;

    // If there are any errors, break out of the loop.
    if( !bResults ) 
        bDone = TRUE;
  }

  // Report any errors.
  if( !bResults )
  {
    DWORD dwLastError = GetLastError( );
    printf( "Error %d has occurred.\n", dwLastError );
  }

  // Close any open handles.
  if( hRequest ) WinHttpCloseHandle( hRequest );
  if( hConnect ) WinHttpCloseHandle( hConnect );
  if( hSession ) WinHttpCloseHandle( hSession );
}

自動ログオン ポリシー

自動ログオン (自動ログオン) ポリシーは、WinHTTP が要求に既定の資格情報を含めることができるタイミングを決定します。 既定の資格情報は、WinHTTP が同期モードまたは非同期モードで使用されているかどうかに応じて、現在のスレッド トークンまたはセッション トークンのいずれかです。 スレッド トークンは同期モードで使用され、セッション トークンは非同期モードで使用されます。 これらの既定の資格情報は、多くの場合、Microsoft Windows にログオンするために使用されるユーザー名とパスワードです。

これらの資格情報が信頼されていないサーバーに対する認証に何気なく使用されないように、自動ログオン ポリシーが実装されました。 既定では、セキュリティ レベルは WINHTTP_AUTOLOGON_SECURITY_LEVEL_MEDIUM に設定されています。これにより、既定の資格情報をイントラネット要求にのみ使用できます。 自動ログオン ポリシーは、NTLM およびネゴシエート認証スキームにのみ適用されます。 資格情報は、他のスキームと共に自動的に送信されることはありません。

自動ログオン ポリシーは、 winHttpSetOption 関数と WINHTTP_OPTION_AUTOLOGON_POLICY フラグを使用して設定できます。 このフラグは、要求ハンドルにのみ適用されます。 ポリシーを WINHTTP_AUTOLOGON_SECURITY_LEVEL_LOW に設定すると、既定の資格情報をすべてのサーバーに送信できます。 ポリシーが WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH に設定されている場合、既定の資格情報を認証に使用することはできません。 MEDIUM レベルで自動ログオンを使用することを強くお勧めします。

格納されたユーザー名およびパスワード

Windows XP では、保存されたユーザー名とパスワードの概念が導入されました。 ユーザーの Passport 資格情報が Passport 登録ウィザード または標準の 資格情報ダイアログで保存されている場合は、保存されたユーザー名とパスワードに保存されます。 Windows XP 以降で WinHTTP を使用する場合、資格情報が明示的に設定されていない場合、WinHTTP は格納されているユーザー名とパスワードの資格情報を自動的に使用します。 これは、NTLM/Kerberos の既定のログオン資格情報のサポートに似ています。 ただし、既定の Passport 資格情報の使用は、自動ログオン ポリシー設定の対象になりません。