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

비밀번호

댓글 작성자
 




... 91  92  93  94  95  96  97  98  99  100  101  102  103  [104]  105  ...
NoWriterDateCnt.TitleFile(s)
11325정성태10/14/201719259.NET Framework: 689. CLR 4.0 환경에서 DLL 모듈의 로드 주소(Base address) 알아내는 방법
11324정성태10/13/201720848디버깅 기술: 101. windbg - "*** WARNING: Unable to verify checksum for" 경고 없애는 방법
11322정성태10/13/201718267디버깅 기술: 100. windbg - .NET 4.0 응용 프로그램의 Main 메서드에 Breakpoint 걸기
11321정성태10/11/201719786.NET Framework: 688. NGen 모듈과 .NET Profiler
11320정성태10/11/201720572.NET Framework: 687. COR_PRF_USE_PROFILE_IMAGES 옵션과 NGen의 "profiler-enhanced images" [1]
11319정성태10/11/201728159.NET Framework: 686. C# - string 배열을 담은 구조체를 직렬화하는 방법
11318정성태10/7/201720923VS.NET IDE: 122. 비주얼 스튜디오에서 관리자 권한을 요구하는 C# 콘솔 프로그램 제작 [1]
11317정성태10/4/201726091VC++: 120. std::copy 등의 함수 사용 시 _SCL_SECURE_NO_WARNINGS 에러 발생
11316정성태9/30/201724136디버깅 기술: 99. (닷넷) 프로세스(EXE)에 디버거가 연결되어 있는지 아는 방법 [4]
11315정성태9/29/201740221기타: 68. "시작하세요! C# 6.0 프로그래밍: 기본 문법부터 실전 예제까지" 구매하신 분들을 위한 C# 7.0/7.1 추가 문법 PDF [8]
11314정성태9/28/201721997디버깅 기술: 98. windbg - 덤프 파일로부터 닷넷 버전 확인하는 방법
11313정성태9/25/201719292디버깅 기술: 97. windbg - 메모리 덤프로부터 DateTime 형식의 값을 알아내는 방법파일 다운로드1
11312정성태9/25/201722369.NET Framework: 685. C# - 구조체(값 형식)의 필드를 리플렉션을 이용해 값을 바꾸는 방법파일 다운로드1
11311정성태9/20/201716832.NET Framework: 684. System.Diagnostics.Process 객체의 명시적인 해제 권장
11310정성태9/19/201720247.NET Framework: 683. WPF의 Window 객체를 생성했는데 GC 수집 대상이 안 되는 이유 [3]
11309정성태9/13/201718371개발 환경 구성: 335. Octave의 명령 창에서 실행한 결과를 복사하는 방법
11308정성태9/13/201719431VS.NET IDE: 121. 비주얼 스튜디오에서 일부 텍스트 파일을 무조건 메모장으로만 여는 문제파일 다운로드1
11307정성태9/13/201721955오류 유형: 421. System.Runtime.InteropServices.SEHException - 0x80004005
11306정성태9/12/201720004.NET Framework: 682. 아웃룩 사용자를 위한 중국어 스팸 필터 Add-in
11305정성태9/12/201721513개발 환경 구성: 334. 기존 프로젝트를 Visual Studio를 이용해 Github의 신규 생성된 repo에 올리는 방법 [1]
11304정성태9/11/201718643개발 환경 구성: 333. 3ds Max를 Hyper-V VM에서 실행하는 방법
11303정성태9/11/201721939개발 환경 구성: 332. Inno Setup 파일의 관리자 권한을 제거하는 방법
11302정성태9/11/201718169개발 환경 구성: 331. SQL Server Express를 위한 방화벽 설정
11301정성태9/11/201717063오류 유형: 420. SQL Server Express 연결 오류 - A network-related or instance-specific error occurred while establishing a connection to SQL Server.
11300정성태9/10/201720921.NET Framework: 681. dotnet.exe - run, exec, build, restore, publish 차이점 [3]
11299정성태9/9/201719665개발 환경 구성: 330. Hyper-V VM의 Internal Network를 Private 유형으로 만드는 방법
... 91  92  93  94  95  96  97  98  99  100  101  102  103  [104]  105  ...