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

비밀번호

댓글 작성자
 




... 136  137  138  139  140  141  142  143  144  145  146  [147]  148  149  150  ...
NoWriterDateCnt.TitleFile(s)
1378정성태11/22/201231643Java: 14. 안드로이드 - Hello World 실습 [7]
1377정성태11/19/201225064.NET Framework: 344. 닷넷 프로파일러 - ICorProfilerInfo::GetILFunctionBody 함수 버그
1376정성태11/15/201230286디버깅 기술: 51. 닷넷 응용 프로그램에서 특정 예외가 발생했을 때 풀 덤프 받는 방법 [6]
1375정성태11/15/201226031디버깅 기술: 50. windbg의 mscordacwks DLL 로드 문제 - 두 번째 이야기
1374정성태11/13/201224130개발 환경 구성: 175. Visual Studio의 "Extension Manager"에서 설치된 구성 요소들의 제거 버튼이 비활성화되었다면!
1373정성태11/13/201224641.NET Framework: 343. VB.NET 어셈블리의 .NET Reflector 소스 코드를 분석할 때 알아두면 좋은 사항
1372정성태11/1/2012119523Windows: 67. 64비트 윈도우에서 Internet Explorer 10이 항상 64비트로만 실행된다면? [57]
1371정성태10/31/201227205.NET Framework: 342. Python의 zip과 with 문 context를 C#과 비교하면. [3]파일 다운로드1
1370정성태10/31/201222457VS.NET IDE: 75. Visual Studio - "Active Solution Platform" 변경을 툴바에서 하는 방법
1369정성태10/31/201235794개발 환경 구성: 174. 윈도우에서 Mono 개발 환경 구성 [4]
1368정성태10/31/201227089개발 환경 구성: 173. Windows Phone SDK 8.0 설치
1367정성태10/30/201234794개발 환경 구성: 172. IIS 7.5부터 지원되는 웹 사이트 자동 시작 모드 [1]
1366정성태10/24/201226140개발 환경 구성: 171. GTK+를 윈도우 환경에 수작업 설치
1365정성태10/24/201224995개발 환경 구성: 170. 우분투 데스크톱 Active Directory 가입하기 [2]
1364정성태10/19/201221388Windows: 66. Hyper-V 2012에서 별도의 네트워크 카드를 이용한 Live Migration
1363정성태10/16/201228857개발 환경 구성: 169. Objective-C의 대안 - Xamarin의 Mono를 이용한 C# iOS 개발 환경 [2]
1362정성태10/16/201229163개발 환경 구성: 168. 우분투 서버 Active Directory 가입하기
1361정성태10/12/201222559.NET Framework: 341. .NET COM+ 를 Managed/Native 클라이언트에서 각각 호출했을 때의 콜 스택 비교 [4]파일 다운로드1
1360정성태10/9/201226941.NET Framework: 340. Windows Server 2012 - .NET Framework 1.x 미지원
1359정성태10/9/201263244Windows: 65. 윈도우 8 - Internet Explorer 10을 32비트 또는 64비트로 통합 [5]
1358정성태9/27/201226431.NET Framework: 339. .NET Profiler 주의 사항 - 하나의 exe 프로세스 내에 다중 .NET 런타임 사용
1357정성태9/27/201223488Windows: 64. Hyper-V - Windows XP의 Live Migration 오류
1356정성태9/26/201229633Windows: 63. 윈도우 서버 2012 - Hyper-V의 새로운 기능 Live Migration [6]
1355정성태9/21/201227622Team Foundation Server: 49. TFS 2012 Express의 필수 보완 작업: 데이터베이스 백업 [1]
1354정성태9/19/201224404.NET Framework: 338. .NET CLR GC 시간 측정하는 방법파일 다운로드1
1353정성태9/17/201225760.NET Framework: 337. Python의 생성기와 코루틴을 C#으로 표현하면. [2]파일 다운로드1
... 136  137  138  139  140  141  142  143  144  145  146  [147]  148  149  150  ...