Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

오류 기록 - MICROSOFT GRAPH – HOW TO IMPLEMENT IAUTHENTICATIONPROVIDER

이번 글은 단순히 다음의 글을 실습하는 과정에서 겪은 오류 기록입니다.

MICROSOFT GRAPH – HOW TO IMPLEMENT IAUTHENTICATIONPROVIDER
; https://adrianszen.com/2019/06/16/microsoft-graph-how-to-implement-iauthenticationprovider/

결론 먼저 말하면, 해당 예제는 정상적으로 동작하지 않는데 아마도 제가 아직 Microsoft Identity Platform에 익숙하지 않아서 그런 것 같습니다. ^^ 위의 예제는 ClientSecrets를 이용하면서도 사용자의 로그인 과정을 필요로 하지 않는 코드만 포함하고 있습니다. 따라서 기존 예제들과는 달리 Access Token이 사용자 문맥을 포함하지 않는 인증을 대표하는 것 같은데 딱히 이것이 아직 어떻게 쓰일 수 있는지 잘 모르겠습니다. (혹시, 위의 예제가 어떤 식으로 활용되는지 아시는 분은 덧글 부탁드립니다.)

그래도, 막상 (성공은 못했지만) 실습하면서 겪은 사항들을 나중에라도 한 번 더 살펴봤을 때 도움이 되도록 기록을 남깁니다.




우선, App 등록을 해야 하는데 그 과정은 아래의 글에서 이미 설명했습니다.

C# - Azure AD 인증을 지원하는 ASP.NET Core/5+ 웹 애플리케이션 예제 구성
; https://www.sysnet.pe.kr/2/0/12614

여기서는 "hMICROSOFT GRAPH – HOW TO IMPLEMENT IAUTHENTICATIONPROVIDER" 글에 따라 다음의 설정이 되었다고 가정하겠습니다.

name: test-auth-webapp
Certificates & secrets: 1개 생성
API permissions: Directry.Read.All, User.Read.All

그다음 간단하게 ASP.NET Core 프로젝트를 하나 만들고 다음의 3개 패키지를 추가합니다.

Install-Package Microsoft.Graph
Install-Package Microsoft.Graph.Core
Install-Package Microsoft.Identity.Client

이후 기본 생성된 코드에 GraphServiceClient를 이용해 Microsoft Graph 쿼리를 발생시키는 코드를 추가합니다.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Graph;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApplication1.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        // ..[생략]...

        private readonly ILogger<WeatherForecastController> _logger;
        private readonly IAuthenticationProvider _auth;

        public WeatherForecastController(ILogger<WeatherForecastController> logger, IAuthenticationProvider auth)
        {
            _logger = logger;
            _auth = auth;
        }

        [HttpGet]
        public async Task<IEnumerable<WeatherForecast>> Get()
        {
            // ..[생략]...

            GraphServiceClient graphClient = new GraphServiceClient("https://graph.microsoft.com/v1.0", _auth);
            var me = await graphClient.Me.Request().GetAsync();

            System.Diagnostics.Trace.WriteLine(me.DisplayName);

            return result;
        }
    }
}

위에서 IAuthenticationProvider를 주입했는데요, 이를 위한 구현 클래스는 다음과 같고,

using Microsoft.Graph;
using Microsoft.Identity.Client;
using System.Net.Http;
using System.Threading.Tasks;

namespace WebApplication1
{
    public class AuthenticationProvider : IAuthenticationProvider
    {
        private readonly string clientId;
        private readonly string clientSecret;
        private readonly string[] appScopes;
        private readonly string tenantId;

        public AuthenticationProvider(string clientId, string clientSecret, string[] appScopes, string tenantId)
        {
            this.clientId = clientId;
            this.clientSecret = clientSecret;
            this.appScopes = appScopes;
            this.tenantId = tenantId;
        }

        public async Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            // Client credential flows in MSAL.NET
            // ; https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Client-credential-flows
            var clientApplication = ConfidentialClientApplicationBuilder.Create(this.clientId)
                .WithClientSecret(this.clientSecret)
                .WithTenantId(this.tenantId)
                .Build();

            var result = await clientApplication.AcquireTokenForClient(this.appScopes).ExecuteAsync();

            if (request.Headers.Contains("Authorization") == false)
            {
                request.Headers.Add("Authorization", result.CreateAuthorizationHeader());
            }
        }
    }
}

이것을 Startup.cs 파일의 ConfigureServices에서 다음과 같이 추가를 하면 됩니다.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    var scopes = new string[]
    {
        "api://...[app_client_id].../.default"
    };

    services.AddSingleton(typeof(IAuthenticationProvider), new AuthenticationProvider)
        (
            "...[app_client_id]...",
            "...[client_secret_value]...",
            scopes,
            "...[tenant_id]..."
        ));
}

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




이렇게 코딩하고 실행해 보면, AuthenticateRequestAsync의 AcquireTokenForClient 호출에서는 정상적으로 Access Token을 받아옵니다. 하지만, 해당 token이 설정된 GraphServiceClient를 사용하는 코드에서,

GraphServiceClient graphClient = new GraphServiceClient("https://graph.microsoft.com/v1.0", _auth);

var sampleRequest = await graphClient.Users.Request().GetAsync(); // 예외 발생

다음과 같은 예외가 발생합니다.

Microsoft.Graph.ServiceException
  HResult=0x80131500
  Message=Code: InvalidAuthenticationToken
Message: Access token validation failure. Invalid audience.
Inner error:
    AdditionalData:
    date: ...[생략]...
    request-id: ...[생략]...
    client-request-id: ...[생략]...
ClientRequestId: ...[생략]...

  Source=Microsoft.Graph.Core
  StackTrace:
   at Microsoft.Graph.HttpProvider.<SendAsync>d__18.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/ExceptionServices/ExceptionDispatchInfo.cs:line 56
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 173
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 150
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult() in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 551
   at Microsoft.Graph.BaseRequest.<SendRequestAsync>d__40.MoveNext()

  This exception was originally thrown at this call stack:
    [External Code]
    System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() in ExceptionDispatchInfo.cs
    System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.ConfiguredTaskAwaitable<TResult>.ConfiguredTaskAwaiter.GetResult() in TaskAwaiter.cs
    [External Code]

실제로 해당 Access Token을 postman으로 테스트를 해보면 동일한 오류 메시지를 받게 됩니다. 그리고 이때의 AcquireTokenForClient의 반환값을 보면,

        AccessToken "ey...[생략]...Ow"    string
        Account null    Microsoft.Identity.Client.IAccount
+       AuthenticationResultMetadata    {Microsoft.Identity.Client.AuthenticationResultMetadata}    Microsoft.Identity.Client.AuthenticationResultMetadata
+       ClaimsPrincipal null    System.Security.Claims.ClaimsPrincipal
+       CorrelationId   {f0...[생략]...63}    System.Guid
+       ExpiresOn   {2021-...[생략]...}   System.DateTimeOffset
+       ExtendedExpiresOn   {2021-...[생략]...}   System.DateTimeOffset
        IdToken null    string
        IsExtendedLifeTimeToken false   bool
-       Scopes  Count = 0x00000001  System.Collections.Generic.IEnumerable<string> {System.Collections.Generic.HashSet<string>}
        [0] "api://32...[생략]...62/.default" string
+       Raw View        
        TenantId    null    string
        TokenType   "Bearer"    string
        UniqueId    null    string
+       Non-Public members      

정상적인 동작을 하던 authResult와는 다른 결과를 갖습니다.




위의 코드 접근을 그대로 따르는 마이크로소프트의 다른 예제를 보면,

Azure-Samples/active-directory-dotnetcore-daemon-v2
; https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2

약간 환경 값이 다릅니다.

// scopes = new string [] { "https://graph.microsoft.com/.default" };

result = await app.AcquireTokenForClient(scopes)
                    .ExecuteAsync();

"MICROSOFT GRAPH – HOW TO IMPLEMENT IAUTHENTICATIONPROVIDER" 글의 전체 예제 코드가 공개되지 않아 scope 값이 "api://{clientId}/.default" 형식이라고 생각했는데 마이크로소프트의 예제에서는 "https://graph.microsoft.com/.default"로 사용하고 있는 것입니다.

하지만, 저렇게 적용을 해도 (역시 Access Token까지는 받아오지만) Microsoft Graph API를 수행할 때 (두 가지 예제 모두) 이런 오류가 발생합니다.

Microsoft.Graph.ServiceException
  HResult=0x80131500
  Message=Code: Authorization_RequestDenied
Message: Insufficient privileges to complete the operation.
Inner error:
    AdditionalData:
    date: ...[생략]...
    request-id: ...[생략]...
    client-request-id: ...[생략]...
ClientRequestId: ...[생략]...

  Source=Microsoft.Graph.Core
  StackTrace:
   at Microsoft.Graph.HttpProvider.<SendAsync>d__18.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/ExceptionServices/ExceptionDispatchInfo.cs:line 56
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 173
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 150
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult() in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 551
   at Microsoft.Graph.BaseRequest.<SendRequestAsync>d__40.MoveNext()

  This exception was originally thrown at this call stack:
    [External Code]
    System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() in ExceptionDispatchInfo.cs
    System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.ConfiguredTaskAwaitable<TResult>.ConfiguredTaskAwaiter.GetResult() in TaskAwaiter.cs
    [External Code]

마이크로소프트의 예제 설명을 보면,

This sample application shows how to use the Microsoft identity platform endpoint to access the data of Microsoft business customers

라고 나오는데, 아무래도 제가 보유하고 있는 Azure Active Directory의 tenant 유형이 아닌 Office 365 같은 식의 tenant에서 발급받은 ClientSecret 값이어야 하지 않나 싶습니다. 이에 대한 힌트를 다음의 글에서 볼 수 있는데요,

Call MS Graph APIs from ASP.NET Core 3.1
; https://cmatskas.com/call-ms-graph-apis-from-asp-net-core-3-1/

"No authentication", "Individual User Accounts", "Work or School Accounts", "Windows Authentication" 4가지인데 아마도 제가 한 환경은 "Individual"인 듯싶고, ... 암튼 이번엔 여기까지 정리하고 다음에 또 시간 나면 접근해 봐야겠습니다. ^^




다음과 같은 오류가 발생한다면?

Microsoft.Identity.Client.MsalServiceException
  HResult=0x80131500
  Message=A configuration issue is preventing authentication - check the error message from the server for details. You can modify the configuration in the application registration portal. See https://aka.ms/msal-net-invalid-client for details.  Original exception: AADSTS7000215: Invalid client secret is provided.
Trace ID: ...[생략]...
Correlation ID: ...[생략]...
Timestamp: ...[생략]...
  Source=Microsoft.Identity.Client
  StackTrace:
   at Microsoft.Identity.Client.OAuth2.OAuth2Client.ThrowServerException(HttpResponse response, RequestContext requestContext)
   at Microsoft.Identity.Client.OAuth2.OAuth2Client.CreateResponse[T](HttpResponse response, RequestContext requestContext)
   at Microsoft.Identity.Client.OAuth2.OAuth2Client.<ExecuteRequestAsync>d__11`1.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/ExceptionServices/ExceptionDispatchInfo.cs:line 56
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 173
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 150
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult() in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs:line 551
   at Microsoft.Identity.Client.OAuth2.OAuth2Client.<GetTokenAsync>d__10.MoveNext()

  This exception was originally thrown at this call stack:
    [External Code]
    System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() in ExceptionDispatchInfo.cs
    System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task) in TaskAwaiter.cs
    System.Runtime.CompilerServices.ConfiguredTaskAwaitable<TResult>.ConfiguredTaskAwaiter.GetResult() in TaskAwaiter.cs
    [External Code]

메시지("Invalid client secret is provided.")에 나오듯이, ClientSecret 값을 잘 못 전달한 것입니다. (아마도 테스트를 이거저거 하면서 저처럼 혼동하면 저 오류를 보게 될 것입니다. ^^)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 8/1/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 181  182  183  184  185  186  187  188  189  190  191  [192]  193  194  195  ...
NoWriterDateCnt.TitleFile(s)
142정성태4/14/200517928    답변글 VS.NET IDE: 36.1. 메모리 구성에 대한 추가 내용
137정성태3/31/200522249.NET Framework: 35. XP SP2 팝업 뚫은 소스
135정성태3/31/200520992VS.NET IDE: 26. SQL Server 2000구성이 실패
134정성태3/31/200518189COM 개체 관련: 16. Microsoft.XMLHTTP 개체에서 Microsoft.XMLDOM 개체를 전송할 때 charset 지정 문제? [2]
128정성태3/30/200516585.NET Framework: 34. VC++에서 Managed 타입의 메서드에 BSTR을 넘기는 경우의 오류(!)
129정성태3/30/200518697    답변글 .NET Framework: 34.1. 위의 질문에 대한 답변으로 나온 것입니다.
130정성태3/30/200515992        답변글 .NET Framework: 34.2. 다시... 제가 질문한 내용입니다. ^^
131정성태3/30/200516531            답변글 .NET Framework: 34.3. 다시... 정봉겸님이 하신... 명확한 답변입니다.
126정성태3/26/200516349.NET Framework: 33. Proxy 환경에서의 Smart Client 업데이트 문제 [1]
133정성태3/31/200517485    답변글 .NET Framework: 33.1. [추가]: Proxy 환경에서의 Smart Client 업데이트 문제 [2]
125정성태3/26/200516416VC++: 15. VC++ Keyword
124정성태3/25/200516966.NET Framework: 32. 네트워크 공유 없이 상대 컴퓨터에 프로그램 설치
119정성태3/21/200516531.NET Framework: 31. 소스세이프 오류현상: 웹 프로젝트를 열수 없습니다.
120정성태3/21/200517875    답변글 .NET Framework: 31.1. 소스세이프 오류현상: PDB 파일이 잠기는 문제
121정성태3/21/200517915    답변글 .NET Framework: 31.2. 소스세이프 오류현상: VS.NET 2003 IDE 와 연동되는 소스세이프 버전 문제
122정성태3/21/200516619    답변글 .NET Framework: 31.3. 소스세이프 관련 사이트
160정성태11/14/200519539    답변글 VS.NET IDE: 31.4. [추가]: 웹 애플리케이션 로드시 "_1"을 붙여서 묻는 경우. [1]
196이문석12/23/200516332        답변글 .NET Framework: 31.8. [답변]: [추가]: 웹 애플리케이션 로드시 "_1" 을 붙여서 묻는 경우.
167정성태10/10/200515891    답변글 .NET Framework: 31.5. [추가]: 삭제한 웹 가상 디렉터리에 대해 동일한 이름으로 웹 공유를 설정할 때 - 이미 있다고 오류발생
190정성태12/11/200515203    답변글 VC++: 31.6. ASP.NET 소스세이프 오류현상: 다른 사람이 체크아웃 한 것을 또 다른 사람이 체크아웃 가능!
191정성태12/11/200517676    답변글 VC++: 31.7. 소스 세이프 사용 시, 특정 프로젝트의 빌드 체크가 솔루션 로드할 때마다 해제되는 경우
118정성태3/30/200623506VC++: 14. TCP through HTTP tunneling: 기업 내 Proxy 서버 제한에서 벗어나는 방법 [2]
117정성태3/19/200524578.NET Framework: 30. Process.Start에서의 인자 길이 제한 [4]
116정성태3/14/200517073.NET Framework: 29. [.NET WebService] 자동생성되는 WSDL 을 막는 방법.
115정성태3/13/200517664VS.NET IDE: 25. [IIS 서버] ODBC 로그 남기기 [1]
195정성태12/21/200516908    답변글 VC++: 25.1. ODBC 로그를 못 남길 때의 오류 화면
... 181  182  183  184  185  186  187  188  189  190  191  [192]  193  194  195  ...