Microsoft オペレーティング システムでユーザー資格情報の認証を行う方法

文書翻訳 文書翻訳
文書番号: 180548 - 対象製品
すべて展開する | すべて折りたたむ

目次

概要

場合によって、アプリケーションでユーザーのユーザー名とパスワードを確認する必要が生じることがあります (ここでは、ユーザー名とパスワードを資格情報と呼びます)。この検証を行うには、アプリケーションを実行するオペレーティング システムに応じて、いくつかの方法があります。

この資料では、ユーザーの資格情報を検証するための一般的な方法をすべて紹介し、それぞれの方法の要件について説明します。

: ユーザー モード アプリケーションでユーザーの資格情報を収集すると、ユーザーを煩わせる場合や、エンタープライズ コンピューティング環境においてセキュリティ ホールとなる可能性があります。まさにこの理由により、Microsoft BackOffice ロゴを取得するための要件に Unified Logon (統合ログオン) が追加されました。統合ログオンでは、Ctrl + Alt + Del キーを押して表示されるログオン画面以外では資格情報の入力は求められません。資格情報の収集が不可欠であることと、その他の方法によるクライアント/サーバー認証は適していないことを確認しておくことが重要です。偽装、および、セキュリティで保護されたサーバーのプログラミングの関連情報については、プラットフォーム SDK のセキュリティに関するドキュメントを参照してください。

詳細

Windows NT 3.51 以降では、LogonUser という API およびその説明が提供されており、ユーザー資格情報の確認によく使用されています。この API は、Windows NT、Windows 2000 および Windows XP で使用することができます。ただし、LogonUser の使用については、いくつかの制限事項があり、場合によってはこれを満たすことが容易でないことがあります。

最も重要な、1 つ目の制限事項は、Windows NT および Windows 2000 では、LogonUser の呼び出し元のプロセスが SE_TCB_NAME の特権を持つ必要があることです (この特権は、ユーザー マネージャでは、[オペレーティング システムの一部として機能] と表示されます)。SE_TCB_NAME の特権は非常に強力であるため、資格情報の認証が必要なアプリケーションを実行できるようにする目的で、任意のユーザーに付与することはお勧めしません。推奨される方法は、ローカル システム アカウントで実行されているサービスから LogonUser を呼び出すことです。この方法が推奨される理由は、ローカル システム アカウントには、SE_TCB_NAME の特権が事前に付与されているためです。

: Microsoft Windows Server 2003 では、LogonUser Win32 API の呼び出しに TCB の特権は必要ありません。ただし、下位互換性を維持するためにこの方法を使用することをお勧めします。

Windows XP では、LogonUser を呼び出すプロセスに SE_TCB_NAME の特権が付与されている必要はありません。このため、Windows XP では、ユーザーの資格情報を認証するための最も簡単な方法は LogonUser API を呼び出すことです。

LogonUser に関するもう 1 つの問題点は、Windows 95、Windows 98 および Windows Millennium Edition (Me) ではこの API が実装されていないことです。

もう 1 つの方法は、セキュリティ サポート プロバイダ インターフェイス (SSPI) を使用して、提供されたユーザーの資格情報でネットワーク型のログオンを実行することです。この認証方法の長所は、特殊な特権が不要であること、および、すべてのバージョンの Windows で使用できることです。資格情報の認証に SSPI を使用した場合、結果として、ログオン タイプとして LOGON32_LOGON_NETWORK を指定して LogonUser API を呼び出した場合と同様のログオンが実行されます。このログオン方法の最大の問題点は、ネットワーク型のログオンを偽装した後はリモート ネットワーク リソースにアクセスできないことです。Windows NT で委任を実行できない問題を回避するために、アプリケーションでログオン タイプ LOGON32_LOGON_INTERACTIVE を指定して LogonUser を呼び出している場合、SSPI のログオン (認証) は、多くの場合、代替策としては使用できません。

以下のサンプル コードは、SSPI サービスを呼び出して資格情報の認証を実行する方法を示したものです。

Windows 95、Windows 98 および Windows Millennium Edition (Me) でこの方法を使用するには、NTLM セキュリティ サービスを有効にする必要があります。このサービスを有効にするには、コントロール パネルを開き、[ネットワーク] をダブルクリックし、[アクセスの制御] タブまたは [アクセス権の管理] タブをクリックし、[ユーザー レベルでアクセスを制御する] または [ユーザー レベルのアクセス管理] をクリックします。

Windows XP では、レジストリ値 ForceGuest に 1 を設定します。このレジストリ値は、デフォルトでは以下のレジストリ キーの下にあります。
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa
ワークグループのメンバの Windows XP コンピュータでの動作は以下のとおりです。
  • ForceGuest に 1 (有効) を設定すると、SSPI では常に Guest アカウントを使用してログオンが実行されます。
  • Guest アカウントが有効な場合、すべてのユーザー資格情報について、SSPI で Guest としてログオンすることができます。
  • Guest アカウントが無効な場合、資格情報が有効でも SSPI ログオンは失敗します。
  • ForceGuest に 0 (無効) を設定すると、SSPI では指定されたユーザーとしてログオンが実行されます。

サンプル コード

/////////////////////////////////////////////////////////////////////////////// 
// 
//  SSPI Authentication Sample
// 
//  This program demonstrates how to use SSPI to authenticate user credentials.
// 
//  THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
//  ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED
//  TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//  PARTICULAR PURPOSE.
// 
//  Copyright (C) 2001.  Microsoft Corporation.  All rights reserved.
/////////////////////////////////////////////////////////////////////////////// 
#define SECURITY_WIN32
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include <sspi.h>
// Older versions of WinError.h does not have SEC_I_COMPLETE_NEEDED #define.
// So, in such SDK environment setup, we will include issperr.h which has the
// definition for SEC_I_COMPLETE_NEEDED. Include issperr.h only if
// SEC_I_COMPLETE_NEEDED is not defined.
#ifndef SEC_I_COMPLETE_NEEDED
#include <issperr.h>
#endif
typedef struct _AUTH_SEQ {
   BOOL fInitialized;
   BOOL fHaveCredHandle;
   BOOL fHaveCtxtHandle;
   CredHandle hcred;
   struct _SecHandle hctxt;
} AUTH_SEQ, *PAUTH_SEQ;
// Function pointers
ACCEPT_SECURITY_CONTEXT_FN       _AcceptSecurityContext     = NULL;
ACQUIRE_CREDENTIALS_HANDLE_FN    _AcquireCredentialsHandle  = NULL;
COMPLETE_AUTH_TOKEN_FN           _CompleteAuthToken         = NULL;
DELETE_SECURITY_CONTEXT_FN       _DeleteSecurityContext     = NULL;
FREE_CONTEXT_BUFFER_FN           _FreeContextBuffer         = NULL;
FREE_CREDENTIALS_HANDLE_FN       _FreeCredentialsHandle     = NULL;
INITIALIZE_SECURITY_CONTEXT_FN   _InitializeSecurityContext = NULL;
QUERY_SECURITY_PACKAGE_INFO_FN   _QuerySecurityPackageInfo  = NULL;
/////////////////////////////////////////////////////////////////////////////// 
void UnloadSecurityDll(HMODULE hModule) {
   if (hModule)
      FreeLibrary(hModule);
   _AcceptSecurityContext      = NULL;
   _AcquireCredentialsHandle   = NULL;
   _CompleteAuthToken          = NULL;
   _DeleteSecurityContext      = NULL;
   _FreeContextBuffer          = NULL;
   _FreeCredentialsHandle      = NULL;
   _InitializeSecurityContext  = NULL;
   _QuerySecurityPackageInfo   = NULL;
}
/////////////////////////////////////////////////////////////////////////////// 
HMODULE LoadSecurityDll() {
   HMODULE hModule;
   BOOL    fAllFunctionsLoaded = FALSE; 
   TCHAR   lpszDLL[MAX_PATH];
   OSVERSIONINFO VerInfo;
   // 
   //  Find out which security DLL to use, depending on
   //  whether we are on NT or Win95 or 2000 or XP or Windows Server 2003
   //  We have to use security.dll on Windows NT 4.0.
   //  All other operating systems, we have to use Secur32.dll
   // 
   VerInfo.dwOSVersionInfoSize = sizeof (OSVERSIONINFO);
   if (!GetVersionEx (&VerInfo))   // If this fails, something has gone wrong
   {
      return FALSE;
   }
   if (VerInfo.dwPlatformId == VER_PLATFORM_WIN32_NT &&
      VerInfo.dwMajorVersion == 4 &&
      VerInfo.dwMinorVersion == 0)
   {
      lstrcpy (lpszDLL, _T("security.dll"));
   }
   else
   {
      lstrcpy (lpszDLL, _T("secur32.dll"));
   }
   hModule = LoadLibrary(lpszDLL);
   if (!hModule)
      return NULL;
   __try {
      _AcceptSecurityContext = (ACCEPT_SECURITY_CONTEXT_FN) 
            GetProcAddress(hModule, "AcceptSecurityContext");
      if (!_AcceptSecurityContext)
         __leave;
#ifdef UNICODE
      _AcquireCredentialsHandle = (ACQUIRE_CREDENTIALS_HANDLE_FN)
            GetProcAddress(hModule, "AcquireCredentialsHandleW");
#else
      _AcquireCredentialsHandle = (ACQUIRE_CREDENTIALS_HANDLE_FN)
            GetProcAddress(hModule, "AcquireCredentialsHandleA");
#endif
      if (!_AcquireCredentialsHandle)
         __leave;
      // CompleteAuthToken is not present on Windows 9x Secur32.dll
      // Do not check for the availablity of the function if it is NULL;
      _CompleteAuthToken = (COMPLETE_AUTH_TOKEN_FN) 
            GetProcAddress(hModule, "CompleteAuthToken");
      _DeleteSecurityContext = (DELETE_SECURITY_CONTEXT_FN) 
            GetProcAddress(hModule, "DeleteSecurityContext");
      if (!_DeleteSecurityContext)
         __leave;
      _FreeContextBuffer = (FREE_CONTEXT_BUFFER_FN) 
            GetProcAddress(hModule, "FreeContextBuffer");
      if (!_FreeContextBuffer)
         __leave;
      _FreeCredentialsHandle = (FREE_CREDENTIALS_HANDLE_FN) 
            GetProcAddress(hModule, "FreeCredentialsHandle");
      if (!_FreeCredentialsHandle)
         __leave;
#ifdef UNICODE
      _InitializeSecurityContext = (INITIALIZE_SECURITY_CONTEXT_FN)
            GetProcAddress(hModule, "InitializeSecurityContextW");
#else
      _InitializeSecurityContext = (INITIALIZE_SECURITY_CONTEXT_FN) 
            GetProcAddress(hModule, "InitializeSecurityContextA");
#endif
      if (!_InitializeSecurityContext)
         __leave;
#ifdef UNICODE
      _QuerySecurityPackageInfo = (QUERY_SECURITY_PACKAGE_INFO_FN)
            GetProcAddress(hModule, "QuerySecurityPackageInfoW");
#else
      _QuerySecurityPackageInfo = (QUERY_SECURITY_PACKAGE_INFO_FN)
            GetProcAddress(hModule, "QuerySecurityPackageInfoA");
#endif
      if (!_QuerySecurityPackageInfo)
         __leave;
      fAllFunctionsLoaded = TRUE;
   } __finally {
      if (!fAllFunctionsLoaded) {
         UnloadSecurityDll(hModule);
         hModule = NULL;
      }
   }
   
   return hModule;
}
/////////////////////////////////////////////////////////////////////////////// 
BOOL GenClientContext(PAUTH_SEQ pAS, PSEC_WINNT_AUTH_IDENTITY pAuthIdentity,
      PVOID pIn, DWORD cbIn, PVOID pOut, PDWORD pcbOut, PBOOL pfDone) {
/*++
 Routine Description:
   Optionally takes an input buffer coming from the server and returns
   a buffer of information to send back to the server.  Also returns
   an indication of whether or not the context is complete.
 Return Value:
   Returns TRUE if successful; otherwise FALSE.
--*/ 
   SECURITY_STATUS ss;
   TimeStamp       tsExpiry;
   SecBufferDesc   sbdOut;
   SecBuffer       sbOut;
   SecBufferDesc   sbdIn;
   SecBuffer       sbIn;
   ULONG           fContextAttr;
   if (!pAS->fInitialized) {
      
      ss = _AcquireCredentialsHandle(NULL, _T("NTLM"), 
            SECPKG_CRED_OUTBOUND, NULL, pAuthIdentity, NULL, NULL,
            &pAS->hcred, &tsExpiry);
      if (ss < 0) {
         fprintf(stderr, "AcquireCredentialsHandle failed with %08X\n", ss);
         return FALSE;
      }
      pAS->fHaveCredHandle = TRUE;
   }
   // Prepare output buffer
   sbdOut.ulVersion = 0;
   sbdOut.cBuffers = 1;
   sbdOut.pBuffers = &sbOut;
   sbOut.cbBuffer = *pcbOut;
   sbOut.BufferType = SECBUFFER_TOKEN;
   sbOut.pvBuffer = pOut;
   // Prepare input buffer
   if (pAS->fInitialized)  {
      sbdIn.ulVersion = 0;
      sbdIn.cBuffers = 1;
      sbdIn.pBuffers = &sbIn;
      sbIn.cbBuffer = cbIn;
      sbIn.BufferType = SECBUFFER_TOKEN;
      sbIn.pvBuffer = pIn;
   }
   ss = _InitializeSecurityContext(&pAS->hcred, 
         pAS->fInitialized ? &pAS->hctxt : NULL, NULL, 0, 0, 
         SECURITY_NATIVE_DREP, pAS->fInitialized ? &sbdIn : NULL,
         0, &pAS->hctxt, &sbdOut, &fContextAttr, &tsExpiry);
   if (ss < 0)  { 
      // <winerror.h>
      fprintf(stderr, "InitializeSecurityContext failed with %08X\n", ss);
      return FALSE;
   }
   pAS->fHaveCtxtHandle = TRUE;
   // If necessary, complete token
   if (ss == SEC_I_COMPLETE_NEEDED || ss == SEC_I_COMPLETE_AND_CONTINUE) {
      if (_CompleteAuthToken) {
         ss = _CompleteAuthToken(&pAS->hctxt, &sbdOut);
         if (ss < 0)  {
            fprintf(stderr, "CompleteAuthToken failed with %08X\n", ss);
            return FALSE;
         }
      }
      else {
         fprintf (stderr, "CompleteAuthToken not supported.\n");
         return FALSE;
      }
   }
   *pcbOut = sbOut.cbBuffer;
   if (!pAS->fInitialized)
      pAS->fInitialized = TRUE;
   *pfDone = !(ss == SEC_I_CONTINUE_NEEDED 
         || ss == SEC_I_COMPLETE_AND_CONTINUE );
   return TRUE;
}
/////////////////////////////////////////////////////////////////////////////// 
BOOL GenServerContext(PAUTH_SEQ pAS, PVOID pIn, DWORD cbIn, PVOID pOut,
      PDWORD pcbOut, PBOOL pfDone) {
/*++
 Routine Description:
    Takes an input buffer coming from the client and returns a buffer
    to be sent to the client.  Also returns an indication of whether or
    not the context is complete.
 Return Value:
    Returns TRUE if successful; otherwise FALSE.
--*/ 
   SECURITY_STATUS ss;
   TimeStamp       tsExpiry;
   SecBufferDesc   sbdOut;
   SecBuffer       sbOut;
   SecBufferDesc   sbdIn;
   SecBuffer       sbIn;
   ULONG           fContextAttr;
   if (!pAS->fInitialized)  {
      
      ss = _AcquireCredentialsHandle(NULL, _T("NTLM"), 
            SECPKG_CRED_INBOUND, NULL, NULL, NULL, NULL, &pAS->hcred, 
            &tsExpiry);
      if (ss < 0) {
         fprintf(stderr, "AcquireCredentialsHandle failed with %08X\n", ss);
         return FALSE;
      }
      pAS->fHaveCredHandle = TRUE;
   }
   // Prepare output buffer
   sbdOut.ulVersion = 0;
   sbdOut.cBuffers = 1;
   sbdOut.pBuffers = &sbOut;
   sbOut.cbBuffer = *pcbOut;
   sbOut.BufferType = SECBUFFER_TOKEN;
   sbOut.pvBuffer = pOut;
   // Prepare input buffer
   sbdIn.ulVersion = 0;
   sbdIn.cBuffers = 1;
   sbdIn.pBuffers = &sbIn;
   sbIn.cbBuffer = cbIn;
   sbIn.BufferType = SECBUFFER_TOKEN;
   sbIn.pvBuffer = pIn;
   ss = _AcceptSecurityContext(&pAS->hcred, 
         pAS->fInitialized ? &pAS->hctxt : NULL, &sbdIn, 0, 
         SECURITY_NATIVE_DREP, &pAS->hctxt, &sbdOut, &fContextAttr, 
         &tsExpiry);
   if (ss < 0)  {
      fprintf(stderr, "AcceptSecurityContext failed with %08X\n", ss);
      return FALSE;
   }
   pAS->fHaveCtxtHandle = TRUE;
   // If necessary, complete token
   if (ss == SEC_I_COMPLETE_NEEDED || ss == SEC_I_COMPLETE_AND_CONTINUE) {
      
      if (_CompleteAuthToken) {
         ss = _CompleteAuthToken(&pAS->hctxt, &sbdOut);
         if (ss < 0)  {
            fprintf(stderr, "CompleteAuthToken failed with %08X\n", ss);
            return FALSE;
         }
      }
      else {
         fprintf (stderr, "CompleteAuthToken not supported.\n");
         return FALSE;
      }
   }
   *pcbOut = sbOut.cbBuffer;
   if (!pAS->fInitialized)
      pAS->fInitialized = TRUE;
   *pfDone = !(ss = SEC_I_CONTINUE_NEEDED 
         || ss == SEC_I_COMPLETE_AND_CONTINUE);
   return TRUE;
}
/////////////////////////////////////////////////////////////////////////////// 
BOOL WINAPI SSPLogonUser(LPTSTR szDomain, LPTSTR szUser, LPTSTR szPassword) {
   AUTH_SEQ    asServer   = {0};
   AUTH_SEQ    asClient   = {0};
   BOOL        fDone      = FALSE;
   BOOL        fResult    = FALSE;
   DWORD       cbOut      = 0;
   DWORD       cbIn       = 0;
   DWORD       cbMaxToken = 0;
   PVOID       pClientBuf = NULL;
   PVOID       pServerBuf = NULL;
   PSecPkgInfo pSPI       = NULL;
   HMODULE     hModule    = NULL;
   SEC_WINNT_AUTH_IDENTITY ai;
   __try {
      hModule = LoadSecurityDll();
      if (!hModule)
         __leave;
      // Get max token size
      _QuerySecurityPackageInfo(_T("NTLM"), &pSPI);
      cbMaxToken = pSPI->cbMaxToken;
      _FreeContextBuffer(pSPI);
      // Allocate buffers for client and server messages
      pClientBuf = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cbMaxToken);
      pServerBuf = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cbMaxToken);
      // Initialize auth identity structure
      ZeroMemory(&ai, sizeof(ai));
#if defined(UNICODE) || defined(_UNICODE)
      ai.Domain = szDomain;
      ai.DomainLength = lstrlen(szDomain);
      ai.User = szUser;
      ai.UserLength = lstrlen(szUser);
      ai.Password = szPassword;
      ai.PasswordLength = lstrlen(szPassword);
      ai.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
#else      
      ai.Domain = (unsigned char *)szDomain;
      ai.DomainLength = lstrlen(szDomain);
      ai.User = (unsigned char *)szUser;
      ai.UserLength = lstrlen(szUser);
      ai.Password = (unsigned char *)szPassword;
      ai.PasswordLength = lstrlen(szPassword);
      ai.Flags = SEC_WINNT_AUTH_IDENTITY_ANSI;
#endif
      // Prepare client message (negotiate) .
      cbOut = cbMaxToken;
      if (!GenClientContext(&asClient, &ai, NULL, 0, pClientBuf, &cbOut, &fDone))
         __leave;
      // Prepare server message (challenge) .
      cbIn = cbOut;
      cbOut = cbMaxToken;
      if (!GenServerContext(&asServer, pClientBuf, cbIn, pServerBuf, &cbOut, 
            &fDone))
         __leave;
         // Most likely failure: AcceptServerContext fails with SEC_E_LOGON_DENIED
         // in the case of bad szUser or szPassword.
         // Unexpected Result: Logon will succeed if you pass in a bad szUser and 
         // the guest account is enabled in the specified domain.
      // Prepare client message (authenticate) .
      cbIn = cbOut;
      cbOut = cbMaxToken;
      if (!GenClientContext(&asClient, &ai, pServerBuf, cbIn, pClientBuf, &cbOut,
            &fDone))
         __leave;
      // Prepare server message (authentication) .
      cbIn = cbOut;
      cbOut = cbMaxToken;
      if (!GenServerContext(&asServer, pClientBuf, cbIn, pServerBuf, &cbOut, 
            &fDone))
         __leave;
      fResult = TRUE;
   } __finally {
      // Clean up resources
      if (asClient.fHaveCtxtHandle)
         _DeleteSecurityContext(&asClient.hctxt);
      if (asClient.fHaveCredHandle)
         _FreeCredentialsHandle(&asClient.hcred);
      if (asServer.fHaveCtxtHandle)
         _DeleteSecurityContext(&asServer.hctxt);
      if (asServer.fHaveCredHandle)
         _FreeCredentialsHandle(&asServer.hcred);
      if (hModule)
         UnloadSecurityDll(hModule);
      HeapFree(GetProcessHeap(), 0, pClientBuf);
      HeapFree(GetProcessHeap(), 0, pServerBuf);
   }
   return fResult;
}
void _tmain(int argc, TCHAR **argv)
{
   if (argc != 4) return;
   // argv[1] - Domain Name
   // argv[2] - User Name
   // argv[3] - Password
   if (SSPLogonUser(argv[1], argv[2], argv[3]))
   {
      printf("User Credentials are valid\n");
   }
   else
      printf("User Credentials are NOT valid\n");
}
				

関連情報

この資料は米国 Microsoft Corporation から提供されている Knowledge Base の Article ID 180548 (最終更新日 2005-03-21) を基に作成したものです。

この資料に含まれているサンプル コード/プログラムは英語版を前提に書かれたものをありのままに記述しており、日本語環境での動作は確認されておりません。

プロパティ

文書番号: 180548 - 最終更新日: 2005年5月20日 - リビジョン: 3.4
この資料は以下の製品について記述したものです。
  • Microsoft Win32 Application Programming Interface?を以下の環境でお使いの場合
    • Microsoft Windows 95
    • Microsoft Windows 98 Standard Edition
    • Microsoft Windows Millennium Edition
    • Microsoft Windows NT 4.0
    • Microsoft Windows 2000 Standard Edition
    • Microsoft Windows XP Professional
キーワード:?
kbhowto kbapi kbkernbase kbsecurity kbfaq KB180548
"Microsoft Knowledge Baseに含まれている情報は、いかなる保証もない現状ベースで提供されるものです。Microsoft Corporation及びその関連会社は、市場性および特定の目的への適合性を含めて、明示的にも黙示的にも、一切の保証をいたしません。さらに、Microsoft Corporation及びその関連会社は、本文書に含まれている情報の使用及び使用結果につき、正確性、真実性等、いかなる表明・保証も行ないません。Microsoft Corporation、その関連会社及びこれらの権限ある代理人による口頭または書面による一切の情報提供またはアドバイスは、保証を意味するものではなく、かつ上記免責条項の範囲を狭めるものではありません。Microsoft Corporation、その関連会社 及びこれらの者の供給者は、直接的、間接的、偶発的、結果的損害、逸失利益、懲罰的損害、または特別損害を含む全ての損害に対して、状況のいかんを問わず一切責任を負いません。(Microsoft Corporation、その関連会社 またはこれらの者の供給者がかかる損害の発生可能性を了知している場合を含みます。) 結果的損害または偶発的損害に対する責任の免除または制限を認めていない地域においては、上記制限が適用されない場合があります。なお、本文書においては、文書の体裁上の都合により製品名の表記において商標登録表示、その他の商標表示を省略している場合がありますので、予めご了解ください。"

フィードバック

 

Contact us for more help

Contact us for more help
Connect with Answer Desk for expert help.
Get more support from smallbusiness.support.microsoft.com